3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PX4 PositionControl を読む:_velocityControl() 前半 — 速度PIDから加速度目標を作るまで

3
Last updated at Posted at 2026-05-15

本連載では、PX4 の PositionControl 全体像を整理し、位置・速度・加速度の setpoint を受け取り、最終的に加速度目標や推力目標へ変換していく流れを見ています。

対象とするコードは、PX4-Autopilot の以下の場所にあります。

PositionControl の主要な処理は、update() 関数を入口として実行されます。

bool PositionControl::update(const float dt)
{
	bool valid = _inputValid();

	if (valid) {
		_positionControl();
		_velocityControl(dt);

		_yawspeed_sp = PX4_ISFINITE(_yawspeed_sp) ? _yawspeed_sp : 0.f;
		_yaw_sp = PX4_ISFINITE(_yaw_sp) ? _yaw_sp : _yaw; // TODO: better way to disable yaw control
	}

	// There has to be a valid output acceleration and thrust setpoint otherwise something went wrong
	return valid && _acc_sp.isAllFinite() && _thr_sp.isAllFinite();
}

そして、前回までで、_positionControl() を読み、位置目標 _pos_sp から速度目標 _vel_sp が作られる流れを見ました。

今回読む範囲

今回は、その次に呼ばれる _velocityControl() を読みます。

void PositionControl::_velocityControl(const float dt)
{
	// Constrain vertical velocity integral
	_vel_int(2) = math::constrain(_vel_int(2), -CONSTANTS_ONE_G, CONSTANTS_ONE_G);

	// PID velocity control
	Vector3f vel_error = _vel_sp - _vel;
	Vector3f acc_sp_velocity = vel_error.emult(_gain_vel_p) + _vel_int - _vel_dot.emult(_gain_vel_d);

	// No control input from setpoints or corresponding states which are NAN
	ControlMath::addIfNotNanVector3f(_acc_sp, acc_sp_velocity);

	_accelerationControl();

	// Integrator anti-windup in vertical direction
	if ((_thr_sp(2) >= -_lim_thr_min && vel_error(2) >= 0.f) ||
	    (_thr_sp(2) <= -_lim_thr_max && vel_error(2) <= 0.f)) {
		vel_error(2) = 0.f;
	}

	// Prioritize vertical control while keeping a horizontal margin
	const Vector2f thrust_sp_xy(_thr_sp);
	const float thrust_sp_xy_norm = thrust_sp_xy.norm();
	const float thrust_max_squared = math::sq(_lim_thr_max);

	// Determine how much vertical thrust is left keeping horizontal margin
	const float allocated_horizontal_thrust = math::min(thrust_sp_xy_norm, _lim_thr_xy_margin);
	const float thrust_z_max_squared = thrust_max_squared - math::sq(allocated_horizontal_thrust);

	// Saturate maximal vertical thrust
	_thr_sp(2) = math::max(_thr_sp(2), -sqrtf(thrust_z_max_squared));

	// Determine how much horizontal thrust is left after prioritizing vertical control
	const float thrust_max_xy_squared = thrust_max_squared - math::sq(_thr_sp(2));
	float thrust_max_xy = 0.f;

	if (thrust_max_xy_squared > 0.f) {
		thrust_max_xy = sqrtf(thrust_max_xy_squared);
	}

	// Saturate thrust in horizontal direction
	if (thrust_sp_xy_norm > thrust_max_xy) {
		_thr_sp.xy() = thrust_sp_xy / thrust_sp_xy_norm * thrust_max_xy;
	}

	// Use tracking Anti-Windup for horizontal direction: during saturation, the integrator is used to unsaturate the output
	// see Anti-Reset Windup for PID controllers, L.Rundqwist, 1990
	const Vector2f acc_sp_xy_produced = Vector2f(_thr_sp) * (CONSTANTS_ONE_G / _hover_thrust);

	// The produced acceleration can be greater or smaller than the desired acceleration due to the saturations and the actual vertical thrust (computed independently).
	// The ARW loop needs to run if the signal is saturated only.
	if (_acc_sp.xy().norm_squared() > acc_sp_xy_produced.norm_squared()) {
		const float arw_gain = 2.f / _gain_vel_p(0);
		const Vector2f acc_sp_xy = _acc_sp.xy();

		vel_error.xy() = Vector2f(vel_error) - arw_gain * (acc_sp_xy - acc_sp_xy_produced);
	}

	// Make sure integral doesn't get NAN
	ControlMath::setZeroIfNanVector3f(vel_error);
	// Update integral part of velocity control
	_vel_int += vel_error.emult(_gain_vel_i) * dt;
}

ご覧の通り、_velocityControl() はかなり密度の高い関数です。

でも、この関数の中では、大まかに言うと、

速度目標 `_vel_sp`
  ↓
速度誤差 `vel_error`
  ↓
速度PID
  ↓
加速度目標 `_acc_sp`
  ↓
_accelerationControl()
  ↓
推力目標 `_thr_sp`
  ↓
推力制限
  ↓
anti-windup
  ↓
積分項更新

という処理をしているだけなのです。

ただ、これらをコードベースで説明しようとすると、1回だけでは紙面が足りないので、今回は、前半部分に絞ります。

具体的には次の範囲です。

void PositionControl::_velocityControl(const float dt)
{
	// Constrain vertical velocity integral
	_vel_int(2) = math::constrain(_vel_int(2), -CONSTANTS_ONE_G, CONSTANTS_ONE_G);

	// PID velocity control
	Vector3f vel_error = _vel_sp - _vel;
	Vector3f acc_sp_velocity = vel_error.emult(_gain_vel_p) + _vel_int - _vel_dot.emult(_gain_vel_d);

	// No control input from setpoints or corresponding states which are NAN
	ControlMath::addIfNotNanVector3f(_acc_sp, acc_sp_velocity);

	_accelerationControl();

	// 説明の都合上、今回扱わない後半部分は省略しています。
	...
}

_velocityControl() 前半では、_vel_sp から _acc_sp を作ります。

では、コードを見ていきましょう。

Z方向の積分項を制限する

最初に出てくるのは、Z方向の積分項を制限する処理です。

_vel_int(2) = math::constrain(_vel_int(2), -CONSTANTS_ONE_G, CONSTANTS_ONE_G);

_vel_int は、速度制御の I 項です。

後で見るように、この _vel_int は速度PIDの結果として、加速度目標に加算されます。

Vector3f acc_sp_velocity = vel_error.emult(_gain_vel_p) + _vel_int - _vel_dot.emult(_gain_vel_d);

つまり、_vel_int の単位は速度ではなく、加速度です。

_vel_int : [m/s²]

ここでは、そのうち Z方向成分だけを、-CONSTANTS_ONE_G から +CONSTANTS_ONE_G の範囲に制限しています。

_vel_int.z = [-1G, +1G] に制限

CONSTANTS_ONE_G は重力加速度(9.80665 m/s²)です。

つまりこの処理は、Z方向の積分補正を ±1G 相当の加速度範囲に制限しているだけです。

補足:geo.h

static constexpr float CONSTANTS_ONE_G = 9.80665f;						// m/s^2

TIPS:なぜZ方向の積分項を制限するのか

ホバー推力の推定がずれている場合や外乱がある場合、P項だけでは定常的な速度誤差が残ることがあります。I項はその不足分を少しずつ補正するために必要です。

一方で、このI項は積分であり、それが蓄積され続けると、飽和が解除された後に大きなオーバーシュートを生み出す場合があります。

具体的なシナリオで説明します。

たとえば、強い下降気流にドローンが押されているとします。

  1. 機体が下に押されるので、Z方向に速度誤差が出続ける
  2. I項がその誤差を積分し、どんどん蓄積される
  3. 推力は上限に張り付いて(飽和して)、それ以上出せない
  4. それでも誤差が残るので、I項はさらに蓄積され続ける

この状態で突然、下降気流がやむと、

  1. 推力の飽和が解除される
  2. しかしI項にはすでに巨大な値が蓄積されている
  3. その分だけ過剰な上昇推力が出て、機体が目標高度を大きく超えてしまう

これがwindup による オーバーシュートです。

そのため、PX4 はZ方向の積分項 _vel_int.z を ±1G の範囲に制限しています。これは「積分器を止める処理」ではなく、すでに蓄積されている値の大きさを抑える処理です。

なお、推力飽和時に積分を進めないための anti-windup 処理は _velocityControl() 後半にあります。次回扱います。

速度誤差を計算する

次に、速度誤差を計算します。

Vector3f vel_error = _vel_sp - _vel;

これは非常に素直な式です。

速度誤差 = 速度目標 - 現在速度

各変数の意味は次の通りです。

_vel_sp    : 速度目標 [m/s]
_vel       : 現在速度 [m/s]
vel_error  : 速度誤差 [m/s]

速度PIDで加速度目標を作る

次が、今回の中心です。

Vector3f acc_sp_velocity = vel_error.emult(_gain_vel_p) + _vel_int - _vel_dot.emult(_gain_vel_d);

分解すると、次のようになります。

acc_sp_velocity
  = P項 + I項 + D項

それぞれを見ると、

P項 : vel_error.emult(_gain_vel_p)
I項 : _vel_int
D項 : - _vel_dot.emult(_gain_vel_d)

です。

ここで emult() は element-wise multiplication、つまり成分ごとの掛け算です。

通常の行列積ではありません。

vel_error.emult(_gain_vel_p)

= (
    vel_error.x * gain_vel_p.x,
    vel_error.y * gain_vel_p.y,
    vel_error.z * gain_vel_p.z
  )

PX4 では、X/Y/Z 方向ごとに異なるゲインを持てるため、このように成分ごとの掛け算になっています。

P項:速度誤差から加速度を作る

P項は次の部分です。

vel_error.emult(_gain_vel_p)

意味としては、

速度誤差 [m/s] × 速度Pゲイン [1/s]
= 加速度目標 [m/s²]

です。

単位を見ると分かりやすいです。

vel_error   : [m/s]
_gain_vel_p : [1/s]

vel_error * _gain_vel_p : [m/s²]

つまり、速度Pゲインは、

速度誤差をどれくらいの加速度要求に変換するか

を決める係数です。

速度目標に対して現在速度が足りない場合、P項はその差を埋める方向の加速度を要求します。

I項:積分器の状態を加速度目標に足す

I項は次の部分です。

_vel_int

ここで少し用語を整理しておきます。

_vel_int は、速度制御の積分器の状態です。

一方、_gain_vel_i は、その積分器を更新するときに使われる I ゲインです。

_vel_int     : 積分器の状態 [m/s²]
_gain_vel_i  : 積分器更新に使う I ゲイン

今回読んでいる _velocityControl() 前半では、_vel_int はすでに蓄積済みの補正量として使われます。

なお、_vel_int の更新処理は _velocityControl() 後半にあります。次回扱います。

D項:現在加速度を使って減衰を与える

D項は次の部分です。

- _vel_dot.emult(_gain_vel_d)

ここで重要なのは、PX4 が「速度誤差の微分」を直接使っているわけではないという点です。

使っているのは _vel_dot です。

_vel_dot は、現在速度 _vel の時間微分、つまり現在加速度に相当する量です。

_vel_dot : [m/s²]

通常、PID制御のD項というと、誤差の微分を使うイメージがあります。

速度制御であれば、

速度誤差 = 速度目標 - 現在速度

なので、速度誤差の微分は、

速度目標の微分 - 現在速度の微分

になります。

しかし、速度目標 _vel_sp は setpoint です。

setpoint は外部から急に変わることがあります。

もし setpoint 側の微分をそのままD項に入れると、速度目標が急変した瞬間にD項が大きく跳ねる可能性があります。

そこで PX4 では、setpoint 側の微分ではなく、現在速度の微分、つまり measurement 側の微分である _vel_dot を使って減衰を与えています。

式にはマイナスが付いています。

- _vel_dot.emult(_gain_vel_d)

つまり、現在すでに加速している場合、その加速を抑える方向の補正が入ります。

直感的には、

P項:
  速度目標に近づくために加速しろ

D項:
  でも、すでに加速しているなら少し抑えろ

という関係です。

これにより、setpoint の急変に対するD項のスパイクを避けつつ、実際の機体運動に対して減衰を与えることができます。

単位で見る速度PID

ここまでの式を、単位で整理します。

Vector3f acc_sp_velocity = vel_error.emult(_gain_vel_p) + _vel_int - _vel_dot.emult(_gain_vel_d);

各項の単位は次の通りです。

_vel_sp         : [m/s]
_vel            : [m/s]
vel_error       : [m/s]

_gain_vel_p     : [1/s]
P項             : [m/s²]

_vel_int        : [m/s²]
I項             : [m/s²]

_vel_dot        : [m/s²]
_gain_vel_d     : [-]
D項             : [m/s²]

acc_sp_velocity : [m/s²]

最終的に acc_sp_velocity は加速度目標になります。

ここが重要です。

速度PIDの出力は thrust ではない
速度PIDの出力は加速度目標である

この設計により、PX4 は PositionControl の内部で、

位置目標
  ↓
速度目標
  ↓
加速度目標
  ↓
推力目標

という段階的な構造を持っています。

関係するPX4パラメータ

速度制御のゲインは、PX4 パラメータとしてはおおむね次のように対応します。

_gain_vel_p.x/y : MPC_XY_VEL_P_ACC
_gain_vel_i.x/y : MPC_XY_VEL_I_ACC
_gain_vel_d.x/y : MPC_XY_VEL_D_ACC

_gain_vel_p.z   : MPC_Z_VEL_P_ACC
_gain_vel_i.z   : MPC_Z_VEL_I_ACC
_gain_vel_d.z   : MPC_Z_VEL_D_ACC

XY方向とZ方向で、別々の速度制御ゲインを持っている点が重要です。

マルチコプターでは、水平移動と高度制御で力の出し方が異なります。

水平移動では、機体を傾けることで推力に水平成分を作ります。

一方、Z方向では、主に総推力の増減によって上昇・下降を制御します。

そのため、XY方向とZ方向の制御ゲインは分けて調整されます。

加速度setpointに合成する

速度PIDで作った acc_sp_velocity は、そのまま _acc_sp に加算されます。

ControlMath::addIfNotNanVector3f(_acc_sp, acc_sp_velocity);

ここで _acc_sp は、加速度 setpoint です。

_acc_sp : 加速度目標 [m/s²]

この処理は、単純な代入ではなく、加算です。

_acc_sp に acc_sp_velocity を加える

なぜ加算なのかというと、PX4 の PositionControl では、外部から acceleration setpoint / feed-forward が与えられる場合があるからです。

つまり _acc_sp には、すでに外部から与えられた加速度 feed-forward が入っている可能性があります。

そこに、速度PID由来の加速度目標 acc_sp_velocity を加算します。

外部 acceleration setpoint / feed-forward
  +
速度制御由来の加速度目標
  =
最終的な加速度目標 `_acc_sp`

ここで足し算には、addIfNotNanVector3f()が使われています。これは、前回で説明しています。

_accelerationControl() に渡す

加速度目標 _acc_sp が作られた後、次に呼ばれるのが _accelerationControl() です。

_accelerationControl();

ここで、ようやく加速度目標が推力目標へ変換されます。

今回の記事では _accelerationControl() の中身には深く入りませんが、役割を一言でいうと、

加速度目標 `_acc_sp` から、推力ベクトル `_thr_sp` を生成する処理

です。

より具体的には、PX4 は _acc_sp から、

欲しい加速度
  ↓
その加速度を出すための推力方向
  ↓
body_z
  ↓
collective thrust
  ↓
3D thrust vector `_thr_sp`

という流れで thrust setpoint を作ります。

ここでも重要なのは、PX4 がいきなり roll / pitch や thrust を直接作っているわけではないという点です。

まず加速度目標を作り、その加速度を実現するための推力方向と推力大きさを計算します。

ここまでの流れ

今回読んだ _velocityControl() 前半の流れをまとめると、次のようになります。

1. Z方向の積分項 `_vel_int.z` を ±1G に制限する

2. 速度目標 `_vel_sp` と現在速度 `_vel` から速度誤差を計算する

3. 速度誤差に対して PID 制御を行い、
   速度制御由来の加速度目標 `acc_sp_velocity` を作る

4. `acc_sp_velocity` を `_acc_sp` に加算する

5. `_accelerationControl()` を呼び出し、
   加速度目標から推力目標 `_thr_sp` を生成する

コードとの対応は次の通りです。

// 1. Z方向積分項を制限
_vel_int(2) = math::constrain(_vel_int(2), -CONSTANTS_ONE_G, CONSTANTS_ONE_G);

// 2. 速度誤差
Vector3f vel_error = _vel_sp - _vel;

// 3. 速度PIDで加速度目標を作る
Vector3f acc_sp_velocity =
	vel_error.emult(_gain_vel_p)
	+ _vel_int
	- _vel_dot.emult(_gain_vel_d);

// 4. 加速度setpointに合成する
ControlMath::addIfNotNanVector3f(_acc_sp, acc_sp_velocity);

// 5. 加速度目標から推力目標を作る
_accelerationControl();

次回扱う内容

_accelerationControl() によって _thr_sp が生成された後、さらに次のような処理が続きます。

Z方向の anti-windup
垂直制御を優先した推力制限
水平推力の余裕確保
総推力上限による saturation
水平 tracking anti-windup
速度積分項の更新

次回は、この _velocityControl() 後半を読み、PX4 が推力制限と anti-windup をどのように扱っているかを見ていきます。

なお、_accelerationControl() の中身については、PositionControl 編の最終回で改めて扱います。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?