1+ using StaticArrays
2+
3+ # System: x = [angle; bias], dynamics: ẋ = A_c*x + B_c*u, measurement: y = C*x
4+ # Continuous: A_c = [0 -1; 0 0], B_c = [1; 0]
5+ # Discretized with Ts = 0.005 (200 Hz)
6+
7+ const Ts = 0.005f0
8+ const Ad = @SMatrix [1.0f0 - Ts; 0.0f0 1.0f0 ] # exp(A_c * Ts), exact since A_c² = 0
9+ const Bd = @SVector [Ts, 0.0f0 ] # ∫₀ᵈᵗ exp(A_c*τ) B_c dτ
10+ const Cd = @SMatrix [1.0f0 0.0f0 ] # Measurement matrix (1×2)
11+ const I2 = @SMatrix [1.0f0 0.0f0 ; 0.0f0 1.0f0 ] # Identity for covariance update
12+
13+ """
14+ DiscreteKalmanFilter
15+
16+ Discrete-time Kalman filter for angle estimation from gyro (input) and accelerometer (measurement).
17+ State is `[angle, gyro_bias]`.
18+ """
19+ mutable struct DiscreteKalmanFilter
20+ x:: SVector{2, Float32} # State [angle, bias]
21+ P:: SMatrix{2, 2, Float32, 4} # Covariance
22+ Q:: SMatrix{2, 2, Float32, 4} # Process noise covariance
23+ R:: SMatrix{1, 1, Float32, 1} # Measurement noise covariance
24+ end
25+
26+ """
27+ DiscreteKalmanFilter(; Q_angle=0.001f0, Q_bias=0.005f0, R_angle=0.5)
28+
29+ Create a discrete Kalman filter with specified noise covariances.
30+ Default values from BalanceCar.h.
31+ """
32+ function DiscreteKalmanFilter (; Q_angle:: Float32 = 0.001f0 , Q_bias:: Float32 = 0.005f0 , R_angle:: Float32 = 0.5f0 )
33+ x = @SVector zeros (Float32, 2 )
34+ P = I2
35+ Q = @SMatrix [Q_angle 0.0f0 ; 0.0f0 Q_bias]
36+ R = @SMatrix [R_angle;;]
37+ DiscreteKalmanFilter (x, P, Q, R)
38+ end
39+
40+ """
41+ predict!(kf, u)
42+
43+ Prediction step: propagate state and covariance using gyro measurement `u` as input.
44+
45+ x = A * x + B * u
46+ P = A * P * A' + Q
47+ """
48+ function predict! (kf:: DiscreteKalmanFilter , u:: Float32 )
49+ kf. x = Ad * kf. x + Bd * u
50+ kf. P = Ad * kf. P * Ad' + kf. Q
51+ nothing
52+ end
53+
54+ """
55+ correct!(kf, y)
56+
57+ Update step: correct state and covariance using accelerometer angle measurement `y`.
58+
59+ S = C * P * C' + R
60+ K = P * C' / S
61+ x = x + K * (y - C * x)
62+ P = (I - K * C) * P
63+ """
64+ function correct! (kf:: DiscreteKalmanFilter , y:: Float32 )
65+ # Innovation covariance (1×1 matrix)
66+ S = Cd * kf. P * Cd' + kf. R
67+
68+ # Kalman gain (2×1)
69+ K = kf. P * Cd' / S[1 , 1 ]
70+
71+ # Innovation (scalar wrapped as 1×1 for matrix multiply)
72+ innovation = SA[y] - Cd * kf. x
73+
74+ # State update
75+ kf. x = kf. x + K * innovation
76+
77+ # Covariance update
78+ kf. P = (I2 - K * Cd) * kf. P
79+
80+ nothing
81+ end
82+
83+ """
84+ update!(kf, u, y)
85+
86+ Combined predict + correct step.
87+
88+ - `u`: gyro measurement (angular velocity)
89+ - `y`: accelerometer angle measurement
90+ """
91+ function update! (kf:: DiscreteKalmanFilter , u:: Float32 , y:: Float32 )
92+ predict! (kf, u)
93+ correct! (kf, y)
94+ nothing
95+ end
96+
97+ """
98+ angle(kf)
99+
100+ Get the estimated angle from the filter state.
101+ """
102+ angle (kf:: DiscreteKalmanFilter ) = kf. x[1 ]
103+
104+ """
105+ bias(kf)
106+
107+ Get the estimated gyro bias from the filter state.
108+ """
109+ bias (kf:: DiscreteKalmanFilter ) = kf. x[2 ]
110+
111+ # IMU calibration constants (from original C++ code)
112+ const GYRO_OFFSET = 128.1f0
113+ const GYRO_SCALE = 131.0f0
114+
115+ """
116+ angle, gyro_x, gyro_z = compute_angles(kf, ax, ay, az, gx, gy, gz)
117+
118+ Apply calibration to raw IMU readings and compute
119+ - `angle`: Tilt angle from accelerometer (radians)
120+ - `gyro_x`: Calibrated gyro x (angular velocity) for tile derivative control
121+ - `gyro_z`: Calibrated gyro z (angular velocity) for turn derivative control
122+
123+ Applies calibration to gyro readings and computes angle from accelerometer.
124+
125+ Use with Kalman filter like this:
126+ ```julia
127+ angle, gyro_x, gyro_z = compute_angles(kf, ax, ay, az, gx, gy, gz)
128+ update!(kf, gyro_x, angle)
129+ ```
130+ That is, the gyro x reading is used as control input, and the angle computed from accelerometer as measurement.
131+
132+
133+ # Arguments
134+ - `kf`: DiscreteKalmanFilter instance
135+ - `ax, ay, az`: Raw accelerometer readings (int16)
136+ - `gx, gy, gz`: Raw gyroscope readings (int16)
137+ """
138+ function compute_angles (kf:: DiscreteKalmanFilter , ax:: Integer , ay:: Integer , az:: Integer ,
139+ gx:: Integer , gy:: Integer , gz:: Integer )
140+ # Calculate angle from accelerometer (radians to degrees)
141+ angle = atan (ay, az) * (180.0f0 / pi )
142+
143+ # Apply gyro calibration offset and scale
144+ gyro_x = (gx - GYRO_OFFSET) / GYRO_SCALE
145+
146+
147+ # Store z-axis gyro (for turn control)
148+ gyro_z = - gz / GYRO_SCALE
149+
150+ angle, gyro_x, gyro_z
151+ end
152+
153+
154+
155+ # test
156+ f = DiscreteKalmanFilter ()
157+
158+ m_gyro_x = 0.1f0 # rad/s
159+ m_angle = 0.05f0 # rad
160+ update! (f, m_gyro_x, m_angle)
161+ @show angle (f)
162+ @show bias (f)
0 commit comments