Skip to content

Commit 154955a

Browse files
committed
add translation of original controller
1 parent 958d7ae commit 154955a

2 files changed

Lines changed: 347 additions & 1 deletion

File tree

src/DiscreteKalmanFilter.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ That is, the gyro x reading is used as control input, and the angle computed fro
136136
function compute_angles(kf::IMUKalmanFilter, ax::Integer, ay::Integer, az::Integer,
137137
gx::Integer, gy::Integer, gz::Integer)
138138
# Calculate angle from accelerometer (radians to degrees)
139-
angle = atan(ay, az) * (180.0f0 / pi)
139+
angle = atan(Float32(ay), Float32(az)) * (180.0f0 / pi)
140140

141141
# Apply gyro calibration offset and scale
142142
gyro_x = (gx - GYRO_OFFSET) / GYRO_SCALE

src/balance_original.jl

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
# Balance car controller translated from original Tumbller/BalanceCar.h
2+
# Parallel PD/PI controller architecture with Kalman filter state estimation
3+
include(joinpath(@__DIR__, "DiscreteKalmanFilter.jl"))
4+
export BalanceController, compute_pwm!, handle_motion_mode!, balance_car!
5+
export MotionMode, STOP, START, FORWARD, BACKWARD, LEFT, RIGHT
6+
export car_stop!, apply_motor_output!
7+
8+
# =============================================================================
9+
# Hardware Interface Stubs (override for actual hardware)
10+
# =============================================================================
11+
12+
# Default no-op stubs - override these for actual hardware
13+
function digitalWrite(pin, value)
14+
println("Stub - override for actual hardware")
15+
end
16+
17+
function analogWrite(pin, value)
18+
println("Stub - override for actual hardware")
19+
end
20+
21+
# Hardware pin constants (defaults - override for actual hardware)
22+
const AIN1 = 0
23+
const BIN1 = 0
24+
const PWMA_LEFT = 0
25+
const PWMB_RIGHT = 0
26+
const STBY_PIN = 0
27+
28+
# =============================================================================
29+
# Constants (matching original BalanceCar.h parameters)
30+
# =============================================================================
31+
32+
# PID parameters
33+
const KP_BALANCE = 55.0f0
34+
const KD_BALANCE = 0.75f0
35+
const KP_SPEED = 10.0f0
36+
const KI_SPEED = 0.26f0
37+
const KP_TURN = 2.5f0
38+
const KD_TURN = 0.5f0
39+
40+
# Angle limits (degrees)
41+
const BALANCE_ANGLE_MIN = -22.0f0
42+
const BALANCE_ANGLE_MAX = 22.0f0
43+
44+
# PWM limits
45+
const PWM_MAX = 255.0f0
46+
const PWM_MIN = -255.0f0
47+
48+
# Integral anti-windup limit
49+
const INTEGRAL_LIMIT = 3000.0f0
50+
51+
# Speed control decimation (runs every N cycles)
52+
const SPEED_CONTROL_PERIOD = 8
53+
54+
# =============================================================================
55+
# Motion Mode Enum
56+
# =============================================================================
57+
58+
@enum MotionMode STOP START FORWARD BACKWARD LEFT RIGHT
59+
60+
# =============================================================================
61+
# Controller State Struct
62+
# =============================================================================
63+
64+
mutable struct BalanceController
65+
# Kalman filter for angle estimation
66+
kf::IMUKalmanFilter
67+
68+
# Encoder pulse accumulators (signed based on PWM direction)
69+
encoder_left_pulse::Int
70+
encoder_right_pulse::Int
71+
72+
# Speed control state (PI integrator)
73+
speed_filter::Float32
74+
speed_filter_old::Float32
75+
car_speed_integral::Float32
76+
speed_control_period_count::Int
77+
78+
# Control outputs
79+
speed_control_output::Float32
80+
rotation_control_output::Float32
81+
82+
# Setpoints
83+
setting_car_speed::Int
84+
setting_turn_speed::Int
85+
86+
# PWM outputs
87+
pwm_left::Float32
88+
pwm_right::Float32
89+
90+
# Motion mode
91+
motion_mode::MotionMode
92+
93+
# Calibration offsets
94+
angle_zero::Float32
95+
angular_velocity_zero::Float32
96+
end
97+
98+
"""
99+
BalanceController(; angle_zero=0.0f0, angular_velocity_zero=0.0f0)
100+
101+
Create a balance controller with default parameters matching the original Tumbller code.
102+
"""
103+
function BalanceController(; angle_zero::Float32=0.0f0, angular_velocity_zero::Float32=0.0f0)
104+
BalanceController(
105+
IMUKalmanFilter(), # kf
106+
0, # encoder_left_pulse
107+
0, # encoder_right_pulse
108+
0.0f0, # speed_filter
109+
0.0f0, # speed_filter_old
110+
0.0f0, # car_speed_integral
111+
0, # speed_control_period_count
112+
0.0f0, # speed_control_output
113+
0.0f0, # rotation_control_output
114+
0, # setting_car_speed
115+
0, # setting_turn_speed
116+
0.0f0, # pwm_left
117+
0.0f0, # pwm_right
118+
STOP, # motion_mode
119+
angle_zero,
120+
angular_velocity_zero
121+
)
122+
end
123+
124+
# =============================================================================
125+
# Control Signal Computation (decoupled from motor output)
126+
# =============================================================================
127+
128+
"""
129+
compute_pwm!(ctrl, encoder_count_left, encoder_count_right, ax, ay, az, gx, gy, gz)
130+
131+
Compute PWM control signals based on IMU and encoder data.
132+
Updates `ctrl.pwm_left` and `ctrl.pwm_right` in place.
133+
134+
Returns the Kalman-filtered angle for use in motion mode handling.
135+
136+
# Control Architecture (parallel, not cascade):
137+
- Balance control: PD on tilt angle
138+
- Speed control: PI on wheel speed (runs every 8 cycles)
139+
- Turn control: P + D on turn rate
140+
141+
Final: `pwm = balance - speed ± rotation`
142+
"""
143+
function compute_pwm!(ctrl::BalanceController,
144+
encoder_count_left::Integer, encoder_count_right::Integer,
145+
ax::Integer, ay::Integer, az::Integer,
146+
gx::Integer, gy::Integer, gz::Integer)
147+
148+
# Accumulate encoder pulses with sign based on current PWM direction
149+
ctrl.encoder_left_pulse += ctrl.pwm_left < 0 ? -encoder_count_left : encoder_count_left
150+
ctrl.encoder_right_pulse += ctrl.pwm_right < 0 ? -encoder_count_right : encoder_count_right
151+
152+
# Get calibrated angles from Kalman filter
153+
accel_angle, gyro_x, gyro_z = compute_angles(ctrl.kf, ax, ay, az, gx, gy, gz)
154+
155+
# Update Kalman filter
156+
update!(ctrl.kf, gyro_x, accel_angle)
157+
kalman_angle = angle(ctrl.kf)
158+
159+
# Balance control (PD on tilt angle)
160+
balance_control_output = KP_BALANCE * (kalman_angle - ctrl.angle_zero) +
161+
KD_BALANCE * (gyro_x - ctrl.angular_velocity_zero)
162+
163+
# Speed control (PI, runs every 8 cycles = 40ms at 5ms sample time)
164+
ctrl.speed_control_period_count += 1
165+
if ctrl.speed_control_period_count >= SPEED_CONTROL_PERIOD
166+
ctrl.speed_control_period_count = 0
167+
168+
# Average wheel speed from encoder pulses
169+
car_speed = (ctrl.encoder_left_pulse + ctrl.encoder_right_pulse) * 0.5f0
170+
ctrl.encoder_left_pulse = 0
171+
ctrl.encoder_right_pulse = 0
172+
173+
# Low-pass filter
174+
ctrl.speed_filter = ctrl.speed_filter_old * 0.7f0 + car_speed * 0.3f0
175+
ctrl.speed_filter_old = ctrl.speed_filter
176+
177+
# PI integrator with setpoint
178+
ctrl.car_speed_integral += ctrl.speed_filter - ctrl.setting_car_speed
179+
180+
# Anti-windup
181+
ctrl.car_speed_integral = clamp(ctrl.car_speed_integral, -INTEGRAL_LIMIT, INTEGRAL_LIMIT)
182+
183+
# Speed control output (negative feedback)
184+
ctrl.speed_control_output = -KP_SPEED * ctrl.speed_filter - KI_SPEED * ctrl.car_speed_integral
185+
186+
# Turn control output (computed at same rate as speed control)
187+
ctrl.rotation_control_output = ctrl.setting_turn_speed + KD_TURN * gyro_z
188+
end
189+
190+
# Combine control outputs
191+
ctrl.pwm_left = balance_control_output - ctrl.speed_control_output - ctrl.rotation_control_output
192+
ctrl.pwm_right = balance_control_output - ctrl.speed_control_output + ctrl.rotation_control_output
193+
194+
# Clamp to PWM limits
195+
ctrl.pwm_left = clamp(ctrl.pwm_left, PWM_MIN, PWM_MAX)
196+
ctrl.pwm_right = clamp(ctrl.pwm_right, PWM_MIN, PWM_MAX)
197+
198+
return kalman_angle
199+
end
200+
201+
# =============================================================================
202+
# Motion Mode Handling
203+
# =============================================================================
204+
205+
"""
206+
handle_motion_mode!(ctrl, kalman_angle; key_flag='0')
207+
208+
Handle motion mode logic including angle limit detection and STOP mode behavior.
209+
Modifies `ctrl.pwm_left`, `ctrl.pwm_right`, `ctrl.motion_mode`, and integrator state.
210+
211+
Returns `true` if motors should be stopped (car_stop! should be called).
212+
"""
213+
function handle_motion_mode!(ctrl::BalanceController, kalman_angle::Float32; key_flag::Char='0')
214+
should_stop_motors = false
215+
216+
# Check angle limits - force STOP if exceeded (except during START or STOP modes)
217+
if ctrl.motion_mode != START && ctrl.motion_mode != STOP
218+
if kalman_angle < BALANCE_ANGLE_MIN || kalman_angle > BALANCE_ANGLE_MAX
219+
ctrl.motion_mode = STOP
220+
should_stop_motors = true
221+
end
222+
end
223+
224+
# Handle STOP mode
225+
if ctrl.motion_mode == STOP
226+
if key_flag != '4'
227+
# Full stop - zero everything
228+
ctrl.car_speed_integral = 0.0f0
229+
ctrl.setting_car_speed = 0
230+
ctrl.pwm_left = 0.0f0
231+
ctrl.pwm_right = 0.0f0
232+
should_stop_motors = true
233+
else
234+
# key_flag == '4': Reset state but don't stop motors (allows restart)
235+
ctrl.car_speed_integral = 0.0f0
236+
ctrl.setting_car_speed = 0
237+
ctrl.pwm_left = 0.0f0
238+
ctrl.pwm_right = 0.0f0
239+
end
240+
end
241+
242+
return should_stop_motors
243+
end
244+
245+
# =============================================================================
246+
# Motor Output Functions
247+
# =============================================================================
248+
249+
"""
250+
car_stop!()
251+
252+
Stop both motors by setting PWM to 0 and appropriate direction pins.
253+
"""
254+
function car_stop!()
255+
digitalWrite(AIN1, 1) # HIGH
256+
digitalWrite(BIN1, 0) # LOW
257+
digitalWrite(STBY_PIN, 1) # HIGH (standby off = enabled, but PWM=0)
258+
analogWrite(PWMA_LEFT, 0)
259+
analogWrite(PWMB_RIGHT, 0)
260+
end
261+
262+
"""
263+
apply_motor_output!(pwm_left, pwm_right)
264+
265+
Apply PWM signals to motors based on computed control values.
266+
Handles direction pin setting based on PWM sign.
267+
"""
268+
function apply_motor_output!(pwm_left::Float32, pwm_right::Float32)
269+
# Left motor
270+
if pwm_left < 0
271+
digitalWrite(AIN1, 1)
272+
analogWrite(PWMA_LEFT, round(Int, -pwm_left))
273+
else
274+
digitalWrite(AIN1, 0)
275+
analogWrite(PWMA_LEFT, round(Int, pwm_left))
276+
end
277+
278+
# Right motor
279+
if pwm_right < 0
280+
digitalWrite(BIN1, 1)
281+
analogWrite(PWMB_RIGHT, round(Int, -pwm_right))
282+
else
283+
digitalWrite(BIN1, 0)
284+
analogWrite(PWMB_RIGHT, round(Int, pwm_right))
285+
end
286+
end
287+
288+
# =============================================================================
289+
# Main Update Function
290+
# =============================================================================
291+
292+
"""
293+
balance_car!(ctrl, encoder_count_left, encoder_count_right, ax, ay, az, gx, gy, gz; key_flag='0')
294+
295+
Main control loop update function. Combines:
296+
1. `compute_pwm!` - Calculate control signals
297+
2. `handle_motion_mode!` - Apply motion mode logic
298+
3. `apply_motor_output!` or `car_stop!` - Output to motors
299+
300+
Call this at 200Hz (5ms interval) matching the original Tumbller timer.
301+
302+
# Arguments
303+
- `ctrl`: BalanceController instance
304+
- `encoder_count_left`, `encoder_count_right`: Encoder pulse counts since last call
305+
- `ax, ay, az`: Raw accelerometer readings (Int16)
306+
- `gx, gy, gz`: Raw gyroscope readings (Int16)
307+
- `key_flag`: Remote control key flag (default '0')
308+
"""
309+
function balance_car!(ctrl::BalanceController,
310+
encoder_count_left::Integer, encoder_count_right::Integer,
311+
ax::Integer, ay::Integer, az::Integer,
312+
gx::Integer, gy::Integer, gz::Integer;
313+
key_flag::Char='0')
314+
315+
# Compute control signals
316+
kalman_angle = compute_pwm!(ctrl, encoder_count_left, encoder_count_right,
317+
ax, ay, az, gx, gy, gz)
318+
319+
# Handle motion mode logic
320+
should_stop = handle_motion_mode!(ctrl, kalman_angle; key_flag)
321+
322+
# Apply motor output
323+
if should_stop
324+
car_stop!()
325+
elseif ctrl.motion_mode != STOP
326+
apply_motor_output!(ctrl.pwm_left, ctrl.pwm_right)
327+
end
328+
329+
return nothing
330+
end
331+
332+
333+
# =============================================================================
334+
# Test no error
335+
# =============================================================================
336+
using Test
337+
ctrl = BalanceController()
338+
@test_nowarn balance_car!(ctrl, 1, 2, 3, 4, 5, 6, 7, 8)
339+
@test ctrl.pwm_left == 0 # motion mode = stop
340+
341+
##
342+
343+
ctrl = BalanceController()
344+
ctrl.motion_mode = FORWARD
345+
@test_nowarn compute_pwm!(ctrl, 512, 512, 512, 512, 512, 512, 512, 512)
346+
@test ctrl.pwm_left != 0

0 commit comments

Comments
 (0)