Bang-Bang Control
Implement simple on/off control with hysteresis in Arc
Bang-bang control is the simplest form of closed-loop control: turn something on when a value is too low, off when it’s too high. It’s commonly used for temperature control (heaters, coolers), pressure regulation, and level control.
Basic On/Off Control
Turn a heater on when temperature drops below a setpoint:
func on_off{setpoint f64} (value f64) u8 {
if value < setpoint {
return 1
}
return 0
}
tank_temp -> on_off{setpoint=50.0} -> heater_cmd This turns the heater on whenever temperature drops below 50°C and off when it rises above 50°C. The problem: rapid cycling as the temperature hovers near the setpoint.
Bang-Bang with Hysteresis
Add a deadband to prevent rapid switching:
func bang_bang{
low f64, // turn on below this
high f64 // turn off above this
} (value f64) u8 {
state $= 0
if state == 0 {
// Output is off, turn on if below low threshold
if value < low {
state = 1
}
} else {
// Output is on, turn off if above high threshold
if value > high {
state = 0
}
}
return state
}
tank_temp -> bang_bang{low=48.0, high=52.0} -> heater_cmd The heater turns on when temperature drops below 48°C and stays on until temperature rises above 52°C. The 4°C deadband prevents cycling.
Heater Control Example
A complete heater controller with enable and safety limits:
func heater_control{
setpoint f64,
deadband f64, // half the total deadband (±)
max_temp f64 // safety cutoff
} (temp f64, enable u8) u8 {
state $= 0
// Safety cutoff
if temp > max_temp {
state = 0
return 0
}
// Check if enabled
if enable == 0 {
state = 0
return 0
}
// Bang-bang control
low := setpoint - deadband
high := setpoint + deadband
if state == 0 {
if temp < low {
state = 1
}
} else {
if temp > high {
state = 0
}
}
return state
}
// Use interval and channel config parameters for multi-input control
func heater_ctrl{
temp chan f64,
enable chan u8,
setpoint f64,
deadband f64,
max_temp f64
} () u8 {
state $= 0
t := temp
en := enable
if t > max_temp { state = 0; return 0 }
if en == 0 { state = 0; return 0 }
low := setpoint - deadband
high := setpoint + deadband
if state == 0 {
if t < low { state = 1 }
} else {
if t > high { state = 0 }
}
return state
}
interval{period=50ms} -> heater_ctrl{
temp=tank_temp,
enable=heater_enable,
setpoint=50.0,
deadband=2.0,
max_temp=80.0
} -> heater_cmd The heater maintains 50°C ± 2°C, but shuts off if temperature exceeds 80°C regardless of the setpoint.
Cooling Control
Cooling works the same way with inverted logic:
func cooler_control{
setpoint f64,
deadband f64,
min_temp f64 // don't cool below this
} (temp f64, enable u8) u8 {
state $= 0
// Safety limit
if temp < min_temp {
state = 0
return 0
}
if enable == 0 {
state = 0
return 0
}
high := setpoint + deadband
low := setpoint - deadband
if state == 0 {
// Cooler is off, turn on if above high threshold
if temp > high {
state = 1
}
} else {
// Cooler is on, turn off if below low threshold
if temp < low {
state = 0
}
}
return state
}
// Similar pattern with channel config parameters
func cooler_ctrl{
temp chan f64,
enable chan u8,
setpoint f64,
deadband f64,
min_temp f64
} () u8 {
state $= 0
t := temp
en := enable
if t < min_temp { state = 0; return 0 }
if en == 0 { state = 0; return 0 }
high := setpoint + deadband
low := setpoint - deadband
if state == 0 {
if t > high { state = 1 }
} else {
if t < low { state = 0 }
}
return state
}
interval{period=50ms} -> cooler_ctrl{
temp=tank_temp,
enable=cooler_enable,
setpoint=25.0,
deadband=2.0,
min_temp=5.0
} -> cooler_cmd Pressure Regulation
Maintain tank pressure by controlling a fill valve:
func pressure_regulator{
target f64,
deadband f64,
max_pressure f64
} (pressure f64, enable u8) u8 {
valve_state $= 0
// Over-pressure protection
if pressure > max_pressure {
valve_state = 0
return 0
}
if enable == 0 {
valve_state = 0
return 0
}
low := target - deadband
high := target + deadband
if valve_state == 0 {
if pressure < low {
valve_state = 1
}
} else {
if pressure > high {
valve_state = 0
}
}
return valve_state
}
func pressure_ctrl{
pressure chan f64,
enable chan u8,
target f64,
deadband f64,
max_pressure f64
} () u8 {
state $= 0
p := pressure
en := enable
if p > max_pressure { state = 0; return 0 }
if en == 0 { state = 0; return 0 }
low := target - deadband
high := target + deadband
if state == 0 {
if p < low { state = 1 }
} else {
if p > high { state = 0 }
}
return state
}
interval{period=50ms} -> pressure_ctrl{
pressure=tank_pressure,
enable=press_enable,
target=500.0,
deadband=10.0,
max_pressure=600.0
} -> valve_cmd Dual-Action Control
Some systems have both heating and cooling (or filling and venting):
func dual_control{
setpoint f64,
heat_deadband f64,
cool_deadband f64
} (value f64, enable u8) (heat u8, cool u8) {
heat_state $= 0
cool_state $= 0
if enable == 0 {
heat_state = 0
cool_state = 0
heat = 0
cool = 0
return
}
// Heating control (below setpoint)
heat_on := setpoint - heat_deadband
heat_off := setpoint
if heat_state == 0 {
if value < heat_on {
heat_state = 1
}
} else {
if value > heat_off {
heat_state = 0
}
}
// Cooling control (above setpoint)
cool_off := setpoint
cool_on := setpoint + cool_deadband
if cool_state == 0 {
if value > cool_on {
cool_state = 1
}
} else {
if value < cool_off {
cool_state = 0
}
}
heat = heat_state
cool = cool_state
}
// For dual outputs, use separate control functions
func heat_control{
temp chan f64,
enable chan u8,
setpoint f64,
deadband f64
} () u8 {
state $= 0
t := temp
en := enable
if en == 0 { state = 0; return 0 }
low := setpoint - deadband
if state == 0 {
if t < low { state = 1 }
} else {
if t > setpoint { state = 0 }
}
return state
}
func cool_control{
temp chan f64,
enable chan u8,
setpoint f64,
deadband f64
} () u8 {
state $= 0
t := temp
en := enable
if en == 0 { state = 0; return 0 }
high := setpoint + deadband
if state == 0 {
if t > high { state = 1 }
} else {
if t < setpoint { state = 0 }
}
return state
}
interval{period=50ms} -> heat_control{
temp=tank_temp, enable=temp_enable, setpoint=50.0, deadband=3.0
} -> heater_cmd
interval{period=50ms} -> cool_control{
temp=tank_temp, enable=temp_enable, setpoint=50.0, deadband=3.0
} -> cooler_cmd In dual-action control, the deadbands prevent simultaneous heating and cooling. The system only heats when 3°C below setpoint and only cools when 3°C above.
Periodic Control Loop
For consistent timing, use an interval to trigger the control loop:
func bang_bang_controller{
sensor chan f64,
output chan f64,
low f64,
high f64
} () {
state $= 0
value := sensor
if state == 0 {
if value < low {
state = 1
}
} else {
if value > high {
state = 0
}
}
output = f64(state)
}
// Run control loop at 20Hz (50ms)
interval{period=50ms} -> bang_bang_controller{
sensor=tank_temp,
output=heater_cmd,
low=48.0,
high=52.0
} This ensures the control loop runs at a consistent rate regardless of how often sensor data arrives.
Bang-Bang in Sequences
Use bang-bang control during specific sequence stages:
sequence main {
stage preheat {
// Maintain temperature while pressurizing
tank_temp -> bang_bang{low=45.0, high=55.0} -> heater_cmd,
// Wait for temperature to stabilize
tank_temp > 48 and tank_temp < 52 => next
}
stage pressurize {
// Continue temperature control
tank_temp -> bang_bang{low=45.0, high=55.0} -> heater_cmd,
// Also control pressure
tank_pressure -> bang_bang{low=490.0, high=510.0} -> valve_cmd,
tank_pressure > 500 and tank_temp > 48 => next
}
stage hold {
// Maintain both
tank_temp -> bang_bang{low=45.0, high=55.0} -> heater_cmd,
tank_pressure -> bang_bang{low=490.0, high=510.0} -> valve_cmd,
wait{duration=60s} => next
}
stage complete {
0 -> heater_cmd,
0 -> valve_cmd
}
}
start_cmd => main Bang-bang control is simple but produces oscillating outputs. For smoother control, consider more sophisticated algorithms implemented outside Arc or future Arc library additions for PID control.