🙄

材料系の人間がモーター制御を作り上げるまで

2024/12/20に公開

これは学ロボAdvent Calendar 2024の20日目の記事になります。当日に記事を書くというなかなかにクレイジーなことをしていますので、多分後日修正を入れると思います。ご容赦ください。

学ロボ Advent Calendar 2024

自己紹介

はじめまして、学ロボについに一度も出られなかった千葉大学(22入学)のykcと申します。一応回路屋です。(機械制御もできます。)23の電装系のほんの少しと24のチームリーダーでR1R2ソロ回路&R1設計&R1制御をしてました。

ロボットにかかわるすべての要素において自分の専攻ではないので、誤りが含まれている可能性があります。もし見つけたらご指摘いただけると幸いです。

そんなよわよわな人間が記事書いて本当にそれ大丈夫?と思われるかもしれませんが、一応、実践投入できるくらいにはなっていますので記事を読んで判断してもらえればと思います。

今回は、ロボマスモーターを動かしたときの備忘録兼新入生教育のための資料作りの一環で記事をまとめられたらなと思います。

前提知識

ラプラス変換と伝達関数を理解している。

モーター制御の理論

モーターの制御理論でよく聞くのはPIDだと思います。しかし、実際に制御をする場合、PIDだけでは物足りません。

例えば、ブラシ付きモーターではLAPやSMといった制御を行い、ブラシレスモーターの場合はベクトル制御というものを用いて電力から運動エネルギーへの変換効率と応答性をあげてあげる必要があります。(ロボマスはこの段階までやってある。ありがたい)

ベクトル制御の話は昨年pizacさんが解説なさっているため、そちらを参照していただければと思います。今回はPID制御に主眼を置いて解説を行っていきます。

角速度制御と位置制御のためのPID制御

世の中の学習資料として提供されているものはほとんどが位置制御についてばかりであり、速度制御について情報がなかったため、非常に苦労した記憶があります。今回の制御はありがたいことにある程度単純なモデルから考えることが出来ます。

ベクトル制御は基本的に電流の追従まで行われているため、電流から速度、位置の制御を行います。


角速度制御

1. 制御対象の分析

モーターのモーメントのつり合いは以下の式で表すことが出来ます。

J \frac{d\omega(t)}{dt} = T_m(t) - B\omega(t) - T_L(t)
  • J: 慣性モーメント
  • \omega(t): 角速度 [rad/s]
  • T_m(t): モーターの生成トルク [N·m]
  • B: 粘性摩擦係数 [N·m·s/rad]
  • T_L(t): 負荷トルク [N·m]

ここでは負荷トルク T_L(t) と生成トルク T_m(t) を合わせてモーターにかかるトルク T_{all}(t) とします。また、トルクT_{all}(t)と電流iは基本的に比例します。

T_{all}(t) = k_t \cdot I(t)
  • T_{all}(t): モーターのトルク [N·m]
  • k_t: トルク定数 [N·m/A](モーター固有の定数)
  • I(t): モーターに流れる電流 [A]
J \frac{d\omega(t)}{dt} + B \omega(t) = T_m(t) - T_{ext}(t) = T_{all}(t) = k_t \cdot I (t)

これをラプラス変換すると

J s \Omega(s) + B \Omega(s) = k_t \cdot I (s)

角速度\Omega(s)を電流\cdot I(I)に関して解きます

\Omega(s) = \frac{k_t \cdot I (s)}{J s + B}

よって、モーターの伝達関数は

G(s) = \frac{\Omega(s)}{I (s)} = \frac{k_t}{J s + B}

これから電流と速度は一次遅れの関係にあることが分かります。


2. PID制御器の設計

次にこのような閉ループ制御システムを構築したとします。

  1. 目標角速度 \Omega_r(s) に対して、現在の角速度 \Omega(s)をフィードバック
  2. 制御器 C(s) が偏差(誤差)E(s)を入力とし、出力として電流指令値I(s)を生成
  3. ロボマスが電流指令値I(s)を受け取り、角速度 \Omega(s) を生成
  • \Omega_r(s) :目標角速度
  • \Omega(s) : 現在の角速度
  • C(s)(= \Omega_r(s) - \Omega(s)) :偏差(誤差)(制御器の入力)
  • I(s)(= C(s) E(s)) :電流(制御器の出力)

モーターの出力角速度 \Omega(s) は次の式で表されます。

\Omega(s) = G(s) I(s) = G(s) C(s) E(s)

偏差 E(s) を代入してH(s) = \frac{\Omega(s)}{\Omega_r(s)}の形に直すと、閉ループ伝達関数は次の式で表されます。

H(s) = \frac{\Omega(s)}{\Omega_r(s)} = \frac{G(s) C(s)}{1 + G(s) C(s)}

モーターの開ループ伝達関数G(s) = \frac{k_t}{J s + B}を代入すると

H(s) = \frac{\frac{k_t \cdot C(s)}{J s + B}}{1 + \frac{k_t \cdot C(s)}{J s + B}}
H(s) = \frac{k_t \cdot C(s)}{J s + B + k_t \cdot C(s)}

制御器C(s)にPID制御器を代入します。PID制御器はC(s) = K_p + \frac{K_i}{s} + K_d sと表されます。

H(s) = \frac{k_t \cdot (K_p + \frac{K_i}{s} + K_d s)}{J s + B + k_t \cdot (K_p + \frac{K_i}{s} + K_d s)}

これをまとめると

H(s) = \frac{K_d s^2 + K_p s + K_i}{(K_d + \frac{J}{k_t}) s^2 + (K_p + \frac{B}{k_t}) s + K_i}

閉ループ伝達関数は二次系で動作することが分かります。


H(s)=1の時、目標と出力が一致しているということになるのでK_pK_dが十分大きい場合、非常に良い制御性が得られると考えられます。

しかし、ゲインが非常に高いということは少しの誤差で大きな補正が入るということです。実際の制御ではオーバーシュートという現象が発生し、制御対象の機械を壊してしまうことになります。

特に非連続的な入力の場合、dゲインが悪影響をあたえることがあります。また、一次遅れ系は細かな振動が入ることはまれなのでほとんど使用しません。よって、制御系から外すべきです。


3. アンチワインドアップの導入

先ほどの制御器では積分項があるため定常的な負荷が働いているときに積分項が積みあがってしまい、負荷から解放されたときに定常偏差やオーバーシュートを起こしてしまいます。この現象をワインドアップといい、これを抑制する制御をアンチワインドアップといいます。

これは速度型PI制御を導入することで解決できます。速度型PIとは偏差から操作量の変化分(ΔMV)を求める演算方式のことを言います。何を言っているかわからないと思うので順を追って説明します。

まず、世の中の学習教材になっているものは位置型PIDといわれるものです。自分の調べた範囲では専門書にこの言葉は出てこなかったので、おそらく速度型PIDを説明する際の俗称のようなものだと思います。位置型PIDは目標との誤差に対して比例、積分。微分を行いゲインをかけて足し合わせたものを出力値とします。ワインドアップは制御器に積分項があることで発生しています。

速度型PIDではこの積分項を排除するために、PIDそのものを一度微分して足し合わせた後に積分をすることで積分項を排除しています。通常の位置型PIDに対して微分を行っているので位置の微分である速度型PIDといわれているようです。

微分をしているので、比例、微分、二階微分の制御器を作り、足しあわせたものを積分します。こうすることで制御器の外側に積分を排除することができ、ワインドアップが起きにくくなります。

従って、一回のサイクルで行う計算は以下のようになります。

今回の指定値=前回の指定値+\frac{偏差から出したPID演算値-前回のPID演算値}{2} \cdot T(制御周期)

今回の場合、速度型PIDのメリットはアンチワインドアップだけでなく、積分項の上限値パラメーターをモーターの最大の電流に設定できるところでもあります。つまり、モーター制御に必要な変数が削減できます。UIを作る手間がかなり削減されました。結果的に8個から3個になりました。

4.プログラム例

実際に使用している関数をお見せします。PIのゲインや前回の誤差(vel_e_pre)は副作用的に使用しているので少しわかりにくいかもしれません。制御周期は0.001秒です。

void MotorCtrl::velPID(uint8_t& number, float target){
	e = target - param[number].velocity;
	float de = (e-pidParam[number].vel_e_pre)/0.001;
	float du = pidParam[number].velKp*de + pidParam[number].velKi*e;
	param[number].output = param[number].output + (du + pidParam[number].vel_du_pre)/2*0.001;
        pidParam[number].vel_e_pre = e;
	pidParam[number].vel_du_pre = du;
	if(param[number].motorKinds== Motor::C610){
		if(param[number].output > 10){
			param[number].output = 10;
		}else if(param[number].output < -10){
			param[number].output = -10;
		}
	}else{
		if(param[number].output > 20){
			param[number].output = 20;
		}else if(param[number].output < -20){
			param[number].output = -20;
		}
	}
}

位置制御

時間が無いので省略します。結論を述べると目標を位置、出力を角速度として、同じようにモーメントのつり合いを記述してラプラス変換を行い伝達関数を求めると積分系になります。積分系はP制御で十分に追従可能です。実装例を以下に示します。

float MotorCtrl::posPID(uint8_t& number, float& target){
	return pidParam[number].posKp*(target - param[number].revolution);
}

カスケード制御

カスケード制御とは複数の制御変数を一つの大きな閉ループで制御するのには限界があるので、いくつかの段階に分けて行う制御のことです。つまり、今回はベクトル制御に角速度制御と位置制御という複数の段階に分けてカスケード制御を行っていたわけです。実は私もこのことを知ったのは制御器を完成させた後でした(笑)

終わりに

ちょっと計画性が無さ過ぎて最後が駆け足となってしまいましたが、ひとまず書ききったので満足です。少しでも世のロボコニストの助けになればいいなと思います。

独ステアドカレもあるらしいのでやりたいんですけど、クリスマスまでに間に合うのかな?…

参考資料
速度型PID制御~制御工学の基礎あれこれ~
速度が関わる場合のPID制御。こんなに変わる動作イメージ!
移動ロボットにおけるブラシ付きDCモータのPID制御
SimpleFOCでブラシレスモーターを回す -pizac's diary
初めて学ぶPID制御の基礎

Discussion