mirror of https://github.com/ArduPilot/ardupilot
214 lines
6.1 KiB
Lua
214 lines
6.1 KiB
Lua
--[[
|
|
control throttle on a generator to achieve a target battery voltage
|
|
|
|
This is meant to replace throttle governors on some hybrid drone
|
|
generators. It monitors battery voltage and controls the throttle
|
|
of the generator to maintain a target voltage using a PI controller
|
|
--]]
|
|
-- luacheck: only 0
|
|
|
|
---@diagnostic disable: need-check-nil
|
|
---@diagnostic disable: param-type-mismatch
|
|
|
|
UPDATE_RATE_HZ = 10
|
|
|
|
-- setup a parameter block
|
|
local PARAM_TABLE_KEY = 73
|
|
local PARAM_TABLE_PREFIX = 'GENCTL_'
|
|
assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 30), 'could not add param table')
|
|
|
|
-- bind a parameter to a variable
|
|
function bind_param(name)
|
|
local p = Parameter()
|
|
assert(p:init(name), string.format('could not find %s parameter', name))
|
|
return p
|
|
end
|
|
|
|
-- add a parameter and bind it to a variable
|
|
function bind_add_param(name, idx, default_value)
|
|
assert(param:add_param(PARAM_TABLE_KEY, idx, name, default_value), string.format('could not add param %s', name))
|
|
return bind_param(PARAM_TABLE_PREFIX .. name)
|
|
end
|
|
|
|
GENCTL_ENABLE = bind_add_param('ENABLE', 1, 0)
|
|
if GENCTL_ENABLE:get() <= 0 then
|
|
return
|
|
end
|
|
|
|
-- MIN, MAX and IDLE PWM for throttle output
|
|
GENCTL_PWM_MIN = bind_add_param('PWM_MIN', 2, 1000)
|
|
GENCTL_PWM_MAX = bind_add_param('PWM_MAX', 3, 2000)
|
|
GENCTL_PWM_IDLE = bind_add_param('PWM_IDLE', 4, 1200)
|
|
|
|
-- P and I gains for controller
|
|
GENCTL_PID_P = bind_add_param('PID_P', 5, 0.1)
|
|
GENCTL_PID_I = bind_add_param('PID_I', 6, 0.1)
|
|
|
|
-- maximum I contribution
|
|
GENCTL_PID_IMAX = bind_add_param('PID_IMAX', 7, 1.0)
|
|
|
|
-- RCn_OPTION value for 3 position switch
|
|
GENCTL_RC_FUNC = bind_add_param('RC_FUNC', 8, 300)
|
|
|
|
-- output servo channel that we will control
|
|
GENCTL_THR_CHAN = bind_add_param('THR_CHAN', 9, 0)
|
|
|
|
-- battery index to monitor, 0 is first battery
|
|
GENCTL_BAT_IDX = bind_add_param('BAT_IDX', 10, 0)
|
|
|
|
-- target voltage
|
|
GENCTL_VOLT_TARG = bind_add_param('VOLT_TARG', 11, 0)
|
|
|
|
-- maximum slew rate in percent/second for throttle change
|
|
GENCTL_SLEW_RATE = bind_add_param('SLEW_RATE', 12, 100)
|
|
|
|
local MAV_SEVERITY_INFO = 6
|
|
local MAV_SEVERITY_NOTICE = 5
|
|
local MAV_SEVERITY_EMERGENCY = 0
|
|
|
|
local switch = nil
|
|
local thr_pwm = GENCTL_PWM_MIN:get()
|
|
local last_switch_pos = nil
|
|
|
|
-- constrain a value between limits
|
|
local function constrain(v, vmin, vmax)
|
|
if v < vmin then
|
|
v = vmin
|
|
end
|
|
if v > vmax then
|
|
v = vmax
|
|
end
|
|
return v
|
|
end
|
|
|
|
-- a PI controller implemented as a Lua object
|
|
local function PI_controller(kP,kI,iMax,min,max)
|
|
-- the new instance. You can put public variables inside this self
|
|
-- declaration if you want to
|
|
local self = {}
|
|
|
|
-- private fields as locals
|
|
local _kP = kP
|
|
local _kI = kI
|
|
local _iMax = iMax
|
|
local _min = min
|
|
local _max = max
|
|
local _last_t = nil
|
|
local _I = 0
|
|
local _P = 0
|
|
local _total = 0
|
|
local _counter = 0
|
|
local _target = 0
|
|
local _current = 0
|
|
|
|
-- update the controller.
|
|
function self.update(target, current)
|
|
local now = millis():tofloat() * 0.001
|
|
if not _last_t then
|
|
_last_t = now
|
|
end
|
|
local dt = now - _last_t
|
|
_last_t = now
|
|
local err = target - current
|
|
_counter = _counter + 1
|
|
|
|
local P = _kP:get() * err
|
|
if ((_total < _max and _total > _min) or (_total >= _max and err < 0) or (_total <= _min and err > 0)) then
|
|
_I = _I + _kI:get() * err * dt
|
|
end
|
|
if _iMax:get() > 0 then
|
|
_I = constrain(_I, -_iMax:get(), iMax:get())
|
|
end
|
|
local I = _I
|
|
local ret = P + I
|
|
|
|
_target = target
|
|
_current = current
|
|
_P = P
|
|
_total = ret
|
|
return ret
|
|
end
|
|
|
|
-- reset integrator to an initial value
|
|
function self.reset(integrator)
|
|
_I = integrator
|
|
end
|
|
|
|
-- log the controller internals
|
|
function self.log(name)
|
|
-- allow for an external addition to total
|
|
logger.write(name,'Targ,Curr,P,I,Total','fffff',_target,_current,_P,_I,_total)
|
|
end
|
|
|
|
-- return the instance
|
|
return self
|
|
end
|
|
|
|
local thr_PI = PI_controller(GENCTL_PID_P, GENCTL_PID_I, GENCTL_PID_IMAX, 0, 1)
|
|
local last_pwm = GENCTL_PWM_MIN:get()
|
|
|
|
function update()
|
|
if switch == nil then
|
|
switch = rc:find_channel_for_option(math.floor(GENCTL_RC_FUNC:get()))
|
|
end
|
|
if switch == nil or GENCTL_ENABLE:get() < 1 then
|
|
-- nothing to do
|
|
return
|
|
end
|
|
|
|
local sw_pos = switch:get_aux_switch_pos()
|
|
if sw_pos ~= last_switch_pos then
|
|
if sw_pos == 0 then
|
|
gcs:send_text(MAV_SEVERITY_INFO,"GenCtl: off")
|
|
elseif sw_pos == 1 then
|
|
gcs:send_text(MAV_SEVERITY_INFO,"GenCtl: idle")
|
|
else
|
|
gcs:send_text(MAV_SEVERITY_INFO,"GenCtl: run")
|
|
end
|
|
last_switch_pos = sw_pos
|
|
end
|
|
|
|
if sw_pos == 0 then
|
|
-- force low throttle
|
|
thr_pwm = GENCTL_PWM_MIN:get()
|
|
thr_PI.reset(0)
|
|
elseif sw_pos == 1 then
|
|
-- force idle
|
|
thr_pwm = GENCTL_PWM_IDLE:get()
|
|
thr_PI.reset(0)
|
|
else
|
|
local bat_volt = battery:voltage_resting_estimate(GENCTL_BAT_IDX:get())
|
|
local thr_out = thr_PI.update(GENCTL_VOLT_TARG:get(), bat_volt)
|
|
thr_out = constrain(thr_out, 0, 1)
|
|
thr_pwm = GENCTL_PWM_IDLE:get() + thr_out * (GENCTL_PWM_MAX:get() - GENCTL_PWM_IDLE:get())
|
|
thr_PI.log("GENC")
|
|
end
|
|
local max_change = GENCTL_SLEW_RATE:get() * (GENCTL_PWM_MAX:get() - GENCTL_PWM_MIN:get()) * 0.01 / UPDATE_RATE_HZ
|
|
thr_pwm = constrain(thr_pwm, last_pwm - max_change, last_pwm + max_change)
|
|
last_pwm = thr_pwm
|
|
if GENCTL_THR_CHAN:get() > 0 then
|
|
SRV_Channels:set_output_pwm_chan(GENCTL_THR_CHAN:get()-1, math.floor(thr_pwm))
|
|
end
|
|
end
|
|
|
|
|
|
-- wrapper around update(). This calls update() at 10Hz,
|
|
-- and if update faults then an error is displayed, but the script is not
|
|
-- stopped
|
|
function protected_wrapper()
|
|
local success, err = pcall(update)
|
|
if not success then
|
|
gcs:send_text(MAV_SEVERITY_EMERGENCY, "Internal Error: " .. err)
|
|
-- when we fault we run the update function again after 1s, slowing it
|
|
-- down a bit so we don't flood the console with errors
|
|
--return protected_wrapper, 1000
|
|
return
|
|
end
|
|
return protected_wrapper, 1000/UPDATE_RATE_HZ
|
|
end
|
|
|
|
gcs:send_text(MAV_SEVERITY_INFO,"Loaded gen_control.lua")
|
|
|
|
-- start running update loop
|
|
return protected_wrapper()
|