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