ペンプロッター概要
ARDUINOで制御可能なペンプロッターを製作しました。
先日投稿した XYテーブルに上下するボールペンを取り付け、描画出来る様にしました。フライス,レーザー加工機等も作ってみたいですが、今回は比較的容易で安価なペンプロッターにしました。
プロッターヘッド部構造
X軸(横移動)側に固定したタミヤ製クランクギアの動力によって、スライドガイドを介して取り付けたペンホルダー内のボールペンを上下させます。描画時はペンを下降させ、描画をしない移動時はペンを上昇させます。
クランクギアの回転運動はクランクアーム・リンクバー・ホルダーに固定している連結金具によって、直線運動に変換し伝達します。ペンが下降し描画位置にあることは、 X軸(横移動)側に固定した リードSWが、上下移動する連結金具に固定したネオジム磁石に反応し、OFF→ONに切り換わることで検出します。

簡易構造なので、ペンを描画床面に対し、完全に平行移動させることは難しく、また、特にプロッターヘッド部の位置決め精度は粗く、上下方向ばらつきの吸収が必要と思われます。
手持ちのボールペンを確認すると、Pentel e-ball という製品が柄の部分の太さが均一(Φ11.0mm程度)で摺動構造を作るのに適していると思われました。
下図が実際に製作した上下位置決め誤差吸収の構造です。ホルダーには、ペンの柄の太さよりも、0.1~0.2mm大きな穴を開け、ペンが摺動できる様にしています。
ペン先はネジ式なので取り外して、ホルダー,ワッシャー,ばね,ワッシャーの順で挿入し、ペン先を取り付けると、ばねの力によって、通常ペン先端は最下端にありますが、床面に当たるとばねが縮んで、ある程度の誤差を吸収できる様になります。

上下方向ばらつき吸収構造は作りましたが、やはりある程度の精度で位置決めする必要があります。ステッピングモーターの様に正確な位置決めは出来ませんので、磁力に反応するリードスィッチで下端位置を検出する様にしています。
使う磁石の種類・大きさや取り付け場所等によっても反応する位置が変わる様です。調整方法を考えていなかったので、結構苦労しました。
DCモーター制御
タミヤ製クランクギア についている DCモーターをドライバ(TA7291P)を使用して、ARDUINOで制御します。ちなみに TA7291P は生産終了予定の様ですので、簡単に記載します。
下図でARDUINOは中央にあり、右側が TA7291P ,及びモータとの接続です。左側はペンの上端・下端を検出するリードSW,及びプログラム検証時に使う押し釦SW,LEDとの接続です。

TA7291P への入力とモータ出力です。
(TA 7291Pメーカー仕様書より、引用 )

プロッターヘッド部の制御
プロッターヘッド部を制御する ARDUINO プログラムは下記の通りです。4つのボタン操作とパソコンからのシリアル通信コマンド操作が可能です。
#define LMT_DN 13
#define LMT_UP 12
#define BTN_01 11
#define BTN_02 10
#define BTN_03 4
#define BTN_04 3
#define PIN_VREF 9 // PWM
#define PIN_IN2 8
#define PIN_IN1 7
#define LED_UP 6
#define LED_DN 5
bool pen_dn;
bool pen_up;
bool com_pn_dn;
bool com_pn_up;
// 初期設定
void setup(){
pinMode(LMT_UP,INPUT);
pinMode(LMT_DN,INPUT);
pinMode(BTN_01,INPUT);
pinMode(BTN_02,INPUT);
pinMode(BTN_03,INPUT);
pinMode(BTN_04,INPUT);
pinMode(PIN_IN1,OUTPUT);
pinMode(PIN_IN2,OUTPUT);
pinMode(LED_UP,OUTPUT);
pinMode(LED_DN,OUTPUT);
analogWrite(PIN_VREF,255); // モーターの回転速度を中間にする 1~255
pen_dn = false ;
pen_up = false ;
com_pn_dn = false ;
com_pn_up = false ;
Serial.begin(9600);
while (! Serial) {
delay(1);
}
}
// ペン上下連続動作の停止
void pen_stop(){
int dly_tim = 1 ;
if(pen_dn || com_pn_dn){
dly_tim = 1800 ; // 下降時遅延時間
}else if(pen_up || com_pn_up){
dly_tim = 10 ; // 上昇時遅延時間
}
delay(dly_tim);
mtr_brake(); // ブレーキ(停止)
pen_dn = false ;
pen_up = false ;
com_pn_dn = false ;
com_pn_up = false ;
}
// モーター停止
void mtr_brake(){
digitalWrite(PIN_IN1,HIGH);
digitalWrite(PIN_IN2,HIGH);
}
// モーター回転
void mtr_drive(int drv_dir){
if(drv_dir==0){
digitalWrite(PIN_IN1,HIGH);
digitalWrite(PIN_IN2,LOW);
}else{
digitalWrite(PIN_IN1,LOW);
digitalWrite(PIN_IN2,HIGH);
}
}
// ◆ 連続メインループ ◆
void loop(){
int S_UP ;
int S_DN ;
int B_01 ;
int B_02 ;
int B_03 ;
int B_04 ;
S_UP = digitalRead(LMT_UP) ; // 上端リミットスイッチ状態
S_DN = digitalRead(LMT_DN) ; // 下端リミットスイッチ状態
B_01 = digitalRead(BTN_01) ; // ボタンスィッチ正転(CW)
B_02 = digitalRead(BTN_02) ; // ボタンスィッチ逆転(CCW)
B_03 = digitalRead(BTN_03) ; // ペンダウン
B_04 = digitalRead(BTN_04) ; // ペンアップ
// リミットSW検出状態に応じてLED点灯(上端検出)
if(S_UP == HIGH){
digitalWrite(LED_UP,HIGH) ; // 上端LED点灯
}else{
digitalWrite(LED_UP,LOW) ; // 上端LED消灯
}
// リミットSW検出状態に応じてLED点灯(下端検出)
if(S_DN == HIGH){
digitalWrite(LED_DN,HIGH) ; // 下端LED点灯
}else{
digitalWrite(LED_DN,LOW) ; // 下端LED消灯
}
// 連続処理以外の手動運転
if( !pen_up && !pen_dn && !com_pn_dn && !com_pn_up ){
if(B_01 == HIGH && B_02 != HIGH){
mtr_drive(0); // 正転(CW)
}else if(B_01 != HIGH && B_02 == HIGH){
mtr_drive(1); // 逆転(CCW)
} else {
mtr_brake(); // ブレーキ(停止)
}
}
// 下端検出まで、連続下降処理
if(B_03 == HIGH && B_01 != HIGH && B_02 != HIGH && B_04 != HIGH && S_DN != HIGH && !pen_up && !pen_dn && !com_pn_dn && !com_pn_up){
pen_dn = true ;
mtr_drive(0); // 正転(CW)
}
// 下端未検出になるまで、連続上昇処理
if(B_04 == HIGH && B_01 != HIGH && B_02 != HIGH && B_03 != HIGH && S_DN == HIGH && !pen_up && !pen_dn && !com_pn_dn && !com_pn_up){
pen_up = true ;
mtr_drive(1); // 逆転(CCW)
}
// 連続動作停止
if(((pen_dn || com_pn_dn) && S_DN == HIGH) || ((pen_up || com_pn_up) && S_DN != HIGH) || (B_03 == HIGH && B_04 == HIGH)){
pen_stop();
}
// シリアル通信操作
if ( Serial.available() > 0 ) {
// \r(CR), \r\n(CR+LF) ,\n(LF)
String str = Serial.readStringUntil('\n');
if( pen_up || pen_dn || com_pn_dn || com_pn_up){
Serial.println("BUSY");
} else {
if ( str == "PENDN") {
com_pn_dn = true ;
mtr_drive(0); // 正転(CW)
Serial.println("PENDN");
} else if( str == "PENUP" ){
com_pn_up = true ;
mtr_drive(1); // 逆転(CCW)
Serial.println("PENUP");
} else {
Serial.println("IDLE");
}
}
}
}
ボタン操作、シリアル通信コマンド操作について記載します。
| No | 名称 | 区分 | 内 容 |
| 1 | 手動CW | SW | ボタンを押下時、CW方向に回転(上昇) |
| 2 | 手動CCW | SW | ボタンを押下時、CCW方向に回転(下降) |
| 3 | 自動下降 | SW | ボタン押後、下端リードSWオンまで自動運転 |
| 4 | 自動上昇 | SW | ボタン押後、下端リードSWオフまで自動運転 |
| 5 | リセット | ― | “自動下降” , “自動上昇”同時押で強制リセット |
| 6 | LED上端 | LED | 上端リードSWがオン時点灯 |
| 7 | LED下端 | LED | 下端リードSWがオン時点灯 |
| 8 | “PENDN” | COM | シリアル通信コマンド受信時、自動下降SW押下と同じく、下端リードSWオンまで自動運転。送信元に“PENDN”を返信。 |
| 9 | “PENUP” | COM | シリアル通信コマンド受信時、自動上昇SW押下と同じく、下端リードSWオンまで自動運転。送信元に“PENUP”を返信。 |
| 10 | “CHK” | COM | シリアル通信コマンド受信時、“BUSY”,“IDLE” 状態返信。“IDLE” 時は動作完了状態。自動運転コマンド受信可能。 |
PC側プログラム(エクセルVBA)
XYテーブル(水平移動)とプロッターヘッド部(Z軸)は異なる ARDUINOで制御しています。パソコンからシリアル通信でそれぞれのARDUINOにコマンド送信し、連続的に描画を行います。パソコン側のプログラムは先回投稿で作成したエクセルVBAプログラムにプロッターヘッド部の上昇/下降プログラムを追加しています。

'円弧補間
Private Sub CommandButton4_Click()
Dim sht
Dim res, rtn
Dim cir_R, pos_X, pos_Y
Dim g_CD As String
Dim ln_CNT
sht = ActiveSheet.Name
Worksheets(sht).Range("E21:F30").ClearContents
ln_CNT = 0
Do
'ペンアップ
res = StartSerialComm("PENUP", "COM3")
rtn = chk_status("CHK", "COM3", "IDLE")
cir_R = Trim(Worksheets(sht).Cells(21 + ln_CNT, 3).Value)
pos_X = Trim(Worksheets(sht).Cells(21 + ln_CNT, 4).Value)
pos_Y = 50
If cir_R = "" Or pos_X = "" Then Exit Do
'始点移動
g_CD = "G90G0X" & pos_X & "Y" & pos_Y
Worksheets(sht).Cells(21 + ln_CNT, 5).Value = g_CD
res = StartSerialComm(g_CD, "COM4")
rtn = chk_status("?", "COM4", "Idle")
'ペンダウン
res = StartSerialComm("PENDN", "COM3")
rtn = chk_status("CHK", "COM3", "IDLE")
'円弧動作
g_CD = "G90G03X" & pos_X & "Y" & pos_Y & "I" & cir_R & "J0F1500"
Worksheets(sht).Cells(21 + ln_CNT, 5).Value = g_CD
res = StartSerialComm(g_CD, "COM4")
rtn = chk_status("?", "COM4", "Idle")
Worksheets(sht).Cells(21 + ln_CNT, 6).Value = rtn
DoEvents
ln_CNT = ln_CNT + 1
If ln_CNT >= 10 Then Exit Do
Loop
'原点移動
g_CD = "G90G0X0Y0"
res = StartSerialComm(g_CD, "COM4")
rtn = chk_status("?", "COM4", "Idle")
MsgBox "連続運転完了(" & Trim(Str(ln_CNT)) & ")", vbOKOnly
End Sub
'動作完了待
Private Function chk_status(cmnd As String, com_port As String, chk_wrd)
Dim loop_cnt
Dim res
loop_cnt = 0
Do
Sleep (500)
DoEvents
res = StartSerialComm(cmnd, com_port)
If InStr(res, chk_wrd) Then
chk_status = res
Exit Do
End If
loop_cnt = loop_cnt + 1
If loop_cnt > 100 Then
chk_status = "NG"
End If
Loop
End Function
まとめ
構造的にX軸(横移動)とZ軸(縦移動)のガタつきが大きく、描画精度はもう一つですが、一連の動作を確認することが出来ました。もう少し複雑な形状の描画が出来る様に検討して行きたいと思います。