@@ -25,8 +25,8 @@ component DyadBot3D
2525 rod_right = MultibodyComponents.FixedTranslation(r = [0, 0, -track / 2]) {^rod_right}
2626 "Small mass at the center of the axle"
2727 axis_body = MultibodyComponents.Body(m = 0.01, r_cm = [0, 0, 0]) {^axis_body}
28- "Main body extending upward from the axle"
29- body = MultibodyComponents.BodyShape(m = body_mass, I_11 = 0.01 * 0.05 ^ 2, I_22 = 0.01 * 0.03 ^ 2, I_33 = 0.01 * 0.05 ^ 2, r = [0, body_height, 0] ) {^body}
28+ "Main body extending upward from the axle, with its center of mass in the middle "
29+ body = MultibodyComponents.BodyShape(m = body_mass, r = [0, 2 * body_height, 0], r_cm = [0, body_height, 0], I_11 = 0.01 * 0.05 ^ 2, I_22 = 0.01 * 0.03 ^ 2, I_33 = 0.01 * 0.05 ^ 2) {^body}
3030 damper_left = RotationalComponents.Components.Damper(d = d_wheel) {^damper_left}
3131 damper_right = RotationalComponents.Components.Damper(d = d_wheel) {^damper_right}
3232 "Use a compliant (elastic) vertical wheel-ground contact instead of a rigid contact constraint"
@@ -188,4 +188,208 @@ metadata {
188188 }
189189 }
190190}
191+ end
192+
193+ """
194+ Three-dimensional model of a two-wheeled balancing robot with ideal rolling
195+ contact.
196+
197+ The two wheels form an ideal no-slip rolling axle
198+ (`MultibodyComponents.RollingWheelSet`, which resolves the kinematic loop
199+ through the two wheels internally), and the inverted-pendulum body is mounted
200+ at the center of the axle through a revolute joint about the axle axis. The
201+ motor torque acts between the body and the two wheels: half the commanded
202+ torque drives each wheel and the reaction acts on the body. Viscous motor
203+ friction acts between the body and each wheel in the same split.
204+
205+ For motion in the vertical plane the model is dynamically equivalent to
206+ `PlanarDyadBot`: the two wheels share the planar model's wheel mass `m`, spin
207+ inertia `Iw` and motor friction `d`, and the body parameters are identical.
208+
209+ Inputs and outputs (same interface as `PlanarDyadBot`):
210+ - `torque`: torque applied by the motor between body and wheels
211+ - `x`, `x_dot`: position and velocity of the wheel axis along the ground
212+ - `theta`, `theta_dot`: tilt angle and tilt rate of the body (zero when upright)
213+
214+ The model must be accompanied by a `MultibodyComponents.World` component in
215+ the top level of the enclosing model.
216+ """
217+ component RollingDyadBot3D
218+ wheels = MultibodyComponents.RollingWheelSet(radius = R, m_wheel = m / 2, I_axis = Iw / 2, I_long = Iw / 4, track = track) {^wheels}
219+ "Body tilt joint about the axle axis"
220+ pitch = MultibodyComponents.Revolute(n = [0, 0, 1]) {^pitch}
221+ body = MultibodyComponents.BodyShape(color = [0.2, 0.2, 0.2, 0.9], m = M, r = [0, 2 * L, 0], r_cm = [0, L, 0], I_11 = Ib, I_22 = Ib / 10, I_33 = Ib, radius = 0.03) {^body}
222+ "Splits the motor torque equally between the two wheels"
223+ gain_half = BlockComponents.Math.Gain(k = 0.5) {^gain_half}
224+ torque_left = RotationalComponents.Sources.TorqueSource() {^torque_left}
225+ torque_right = RotationalComponents.Sources.TorqueSource() {^torque_right}
226+ damper_left = RotationalComponents.Components.Damper(d = d / 2) {^damper_left}
227+ damper_right = RotationalComponents.Components.Damper(d = d / 2) {^damper_right}
228+ "Motor torque command"
229+ torque = Dyad.RealInput() {^torque}
230+ "Position of the wheel axis"
231+ x = Dyad.RealOutput() {^x}
232+ "Body tilt angle"
233+ theta = Dyad.RealOutput() {^theta}
234+ "Velocity of the wheel axis"
235+ x_dot = Dyad.RealOutput() {^x_dot}
236+ "Body tilt rate"
237+ theta_dot = Dyad.RealOutput() {^theta_dot}
238+ "Body mass"
239+ parameter M::Mass = 0.1
240+ "Total wheel mass (split between the two wheels)"
241+ parameter m::Mass = 0.05
242+ "Wheel radius"
243+ parameter R::Length = 0.04
244+ "Distance from wheel axis to body center of mass"
245+ parameter L::Length = 0.08
246+ "Body moment of inertia about the axle axis"
247+ parameter Ib::Inertia = 2.5e-4
248+ "Total wheel + motor moment of inertia about the spin axis (split between the two wheels)"
249+ parameter Iw::Inertia = 1e-4
250+ "Motor viscous friction (split between the two wheels)"
251+ parameter d::RotationalDampingConstant = 0.0001
252+ "Initial tilt angle of the body"
253+ parameter phi0::Angle = 0.0
254+ "Distance between the wheels"
255+ parameter track::Length = 0.13
256+ relations
257+ initial pitch.phi = phi0
258+ initial pitch.w = 0
259+ connect(wheels.frame_middle, pitch.frame_a) {^id1}
260+ connect(pitch.frame_b, body.frame_a) {^id2}
261+ connect(torque, gain_half.u) {^id3}
262+ connect(gain_half.y, torque_left.tau, torque_right.tau) {^id4}
263+ connect(torque_left.spline, damper_left.spline_a, wheels.axis1) {^id5}
264+ connect(torque_right.spline, damper_right.spline_a, wheels.axis2) {^id6}
265+ connect(torque_left.support, torque_right.support, damper_left.spline_b, damper_right.spline_b, pitch.axis) {^id7}
266+ x = wheels.x
267+ theta = pitch.phi
268+ x_dot = der(wheels.x)
269+ theta_dot = pitch.w
270+ metadata {
271+ "Dyad": {"icons": {"default": "dyad://DyadBotComponents/DyadBot3D.svg"}},
272+ "_links": {
273+ "wheels": {
274+ "Dyad": {
275+ "placement": {
276+ "diagram": {"iconName": "default", "x1": 400, "y1": 620, "x2": 600, "y2": 820, "rot": 0}
277+ },
278+ "tags": []
279+ }
280+ },
281+ "pitch": {
282+ "Dyad": {"placement": {"diagram": {"x1": 450, "y1": 480, "x2": 550, "y2": 580}}}
283+ },
284+ "body": {
285+ "Dyad": {
286+ "placement": {"diagram": {"x1": 350, "y1": 130, "x2": 650, "y2": 430, "rot": 270}}
287+ }
288+ },
289+ "gain_half": {
290+ "Dyad": {"placement": {"diagram": {"x1": 180, "y1": 460, "x2": 260, "y2": 540}}}
291+ },
292+ "torque_left": {
293+ "Dyad": {"placement": {"diagram": {"x1": 150, "y1": 620, "x2": 250, "y2": 720}}}
294+ },
295+ "torque_right": {
296+ "Dyad": {"placement": {"diagram": {"x1": 850, "y1": 620, "x2": 750, "y2": 720}}}
297+ },
298+ "damper_left": {
299+ "Dyad": {"placement": {"diagram": {"x1": 150, "y1": 760, "x2": 250, "y2": 860}}}
300+ },
301+ "damper_right": {
302+ "Dyad": {"placement": {"diagram": {"x1": 850, "y1": 760, "x2": 750, "y2": 860}}}
303+ },
304+ "torque": {
305+ "Dyad": {
306+ "placement": {
307+ "diagram": {"iconName": "default", "x1": 20, "y1": 460, "x2": 100, "y2": 540, "rot": 0}
308+ },
309+ "tags": []
310+ }
311+ },
312+ "x": {
313+ "Dyad": {
314+ "placement": {
315+ "diagram": {"iconName": "default", "x1": 880, "y1": 300, "x2": 960, "y2": 380, "rot": 0}
316+ },
317+ "tags": []
318+ }
319+ },
320+ "theta": {
321+ "Dyad": {
322+ "placement": {
323+ "diagram": {"iconName": "default", "x1": 880, "y1": 420, "x2": 960, "y2": 500, "rot": 0}
324+ },
325+ "tags": []
326+ }
327+ },
328+ "x_dot": {
329+ "Dyad": {
330+ "placement": {
331+ "diagram": {"iconName": "default", "x1": 880, "y1": 540, "x2": 960, "y2": 620, "rot": 0}
332+ },
333+ "tags": []
334+ }
335+ },
336+ "theta_dot": {
337+ "Dyad": {
338+ "placement": {
339+ "diagram": {"iconName": "default", "x1": 880, "y1": 660, "x2": 960, "y2": 740, "rot": 0}
340+ },
341+ "tags": []
342+ }
343+ },
344+ "id1": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
345+ "id2": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
346+ "id3": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
347+ "id4": {
348+ "Dyad": {
349+ "edges": [
350+ {"S": 1, "M": [], "E": -1},
351+ {"S": -1, "M": [{"x": 200, "y": 670}], "E": 2},
352+ {"S": 3, "M": [{"x": 800, "y": 670}], "E": -1}
353+ ],
354+ "junctions": [{"x": 300, "y": 560}],
355+ "renderStyle": "standard"
356+ }
357+ },
358+ "id5": {
359+ "Dyad": {
360+ "edges": [
361+ {"S": 1, "M": [], "E": -1},
362+ {"S": -1, "M": [], "E": 2},
363+ {"S": 3, "M": [], "E": -1}
364+ ],
365+ "junctions": [{"x": 350, "y": 700}],
366+ "renderStyle": "standard"
367+ }
368+ },
369+ "id6": {
370+ "Dyad": {
371+ "edges": [
372+ {"S": 1, "M": [], "E": -1},
373+ {"S": -1, "M": [], "E": 2},
374+ {"S": 3, "M": [], "E": -1}
375+ ],
376+ "junctions": [{"x": 650, "y": 700}],
377+ "renderStyle": "standard"
378+ }
379+ },
380+ "id7": {
381+ "Dyad": {
382+ "edges": [
383+ {"S": 1, "M": [], "E": -1},
384+ {"S": -1, "M": [], "E": 2},
385+ {"S": 3, "M": [], "E": -1},
386+ {"S": 4, "M": [], "E": -1},
387+ {"S": 5, "M": [], "E": -1}
388+ ],
389+ "junctions": [{"x": 500, "y": 900}],
390+ "renderStyle": "standard"
391+ }
392+ }
393+ }
394+ }
191395end
0 commit comments