@@ -188,4 +188,211 @@ 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+ # A plain Body is used instead of BodyShape: BodyShape places its inner Body
222+ # at translation_cm.frame_b and additionally passes r_cm on to it, so the
223+ # center of mass would end up at twice the requested offset.
224+ body = MultibodyComponents.Body(color = [0.2, 0.2, 0.2, 0.9], m = M, r_cm = [0, L, 0], I_11 = Ib, I_22 = Ib / 10, I_33 = Ib) {^body}
225+ "Splits the motor torque equally between the two wheels"
226+ gain_half = BlockComponents.Math.Gain(k = 0.5) {^gain_half}
227+ torque_left = RotationalComponents.Sources.TorqueSource() {^torque_left}
228+ torque_right = RotationalComponents.Sources.TorqueSource() {^torque_right}
229+ damper_left = RotationalComponents.Components.Damper(d = d / 2) {^damper_left}
230+ damper_right = RotationalComponents.Components.Damper(d = d / 2) {^damper_right}
231+ "Motor torque command"
232+ torque = Dyad.RealInput() {^torque}
233+ "Position of the wheel axis"
234+ x = Dyad.RealOutput() {^x}
235+ "Body tilt angle"
236+ theta = Dyad.RealOutput() {^theta}
237+ "Velocity of the wheel axis"
238+ x_dot = Dyad.RealOutput() {^x_dot}
239+ "Body tilt rate"
240+ theta_dot = Dyad.RealOutput() {^theta_dot}
241+ "Body mass"
242+ parameter M::Mass = 0.1
243+ "Total wheel mass (split between the two wheels)"
244+ parameter m::Mass = 0.05
245+ "Wheel radius"
246+ parameter R::Length = 0.04
247+ "Distance from wheel axis to body center of mass"
248+ parameter L::Length = 0.08
249+ "Body moment of inertia about the axle axis"
250+ parameter Ib::Inertia = 2.5e-4
251+ "Total wheel + motor moment of inertia about the spin axis (split between the two wheels)"
252+ parameter Iw::Inertia = 1e-4
253+ "Motor viscous friction (split between the two wheels)"
254+ parameter d::RotationalDampingConstant = 0.0001
255+ "Initial tilt angle of the body"
256+ parameter phi0::Angle = 0.0
257+ "Distance between the wheels"
258+ parameter track::Length = 0.13
259+ relations
260+ initial pitch.phi = phi0
261+ initial pitch.w = 0
262+ connect(wheels.frame_middle, pitch.frame_a) {^id1}
263+ connect(pitch.frame_b, body.frame_a) {^id2}
264+ connect(torque, gain_half.u) {^id3}
265+ connect(gain_half.y, torque_left.tau, torque_right.tau) {^id4}
266+ connect(torque_left.spline, damper_left.spline_a, wheels.axis1) {^id5}
267+ connect(torque_right.spline, damper_right.spline_a, wheels.axis2) {^id6}
268+ connect(torque_left.support, torque_right.support, damper_left.spline_b, damper_right.spline_b, pitch.axis) {^id7}
269+ x = wheels.x
270+ theta = pitch.phi
271+ x_dot = der(wheels.x)
272+ theta_dot = pitch.w
273+ metadata {
274+ "Dyad": {"icons": {"default": "dyad://DyadBotComponents/DyadBot3D.svg"}},
275+ "_links": {
276+ "wheels": {
277+ "Dyad": {
278+ "placement": {
279+ "diagram": {"iconName": "default", "x1": 400, "y1": 620, "x2": 600, "y2": 820, "rot": 0}
280+ },
281+ "tags": []
282+ }
283+ },
284+ "pitch": {
285+ "Dyad": {"placement": {"diagram": {"x1": 450, "y1": 480, "x2": 550, "y2": 580}}}
286+ },
287+ "body": {
288+ "Dyad": {
289+ "placement": {"diagram": {"x1": 350, "y1": 130, "x2": 650, "y2": 430, "rot": 270}}
290+ }
291+ },
292+ "gain_half": {
293+ "Dyad": {"placement": {"diagram": {"x1": 180, "y1": 460, "x2": 260, "y2": 540}}}
294+ },
295+ "torque_left": {
296+ "Dyad": {"placement": {"diagram": {"x1": 150, "y1": 620, "x2": 250, "y2": 720}}}
297+ },
298+ "torque_right": {
299+ "Dyad": {"placement": {"diagram": {"x1": 850, "y1": 620, "x2": 750, "y2": 720}}}
300+ },
301+ "damper_left": {
302+ "Dyad": {"placement": {"diagram": {"x1": 150, "y1": 760, "x2": 250, "y2": 860}}}
303+ },
304+ "damper_right": {
305+ "Dyad": {"placement": {"diagram": {"x1": 850, "y1": 760, "x2": 750, "y2": 860}}}
306+ },
307+ "torque": {
308+ "Dyad": {
309+ "placement": {
310+ "diagram": {"iconName": "default", "x1": 20, "y1": 460, "x2": 100, "y2": 540, "rot": 0}
311+ },
312+ "tags": []
313+ }
314+ },
315+ "x": {
316+ "Dyad": {
317+ "placement": {
318+ "diagram": {"iconName": "default", "x1": 880, "y1": 300, "x2": 960, "y2": 380, "rot": 0}
319+ },
320+ "tags": []
321+ }
322+ },
323+ "theta": {
324+ "Dyad": {
325+ "placement": {
326+ "diagram": {"iconName": "default", "x1": 880, "y1": 420, "x2": 960, "y2": 500, "rot": 0}
327+ },
328+ "tags": []
329+ }
330+ },
331+ "x_dot": {
332+ "Dyad": {
333+ "placement": {
334+ "diagram": {"iconName": "default", "x1": 880, "y1": 540, "x2": 960, "y2": 620, "rot": 0}
335+ },
336+ "tags": []
337+ }
338+ },
339+ "theta_dot": {
340+ "Dyad": {
341+ "placement": {
342+ "diagram": {"iconName": "default", "x1": 880, "y1": 660, "x2": 960, "y2": 740, "rot": 0}
343+ },
344+ "tags": []
345+ }
346+ },
347+ "id1": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
348+ "id2": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
349+ "id3": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
350+ "id4": {
351+ "Dyad": {
352+ "edges": [
353+ {"S": 1, "M": [], "E": -1},
354+ {"S": -1, "M": [{"x": 200, "y": 670}], "E": 2},
355+ {"S": 3, "M": [{"x": 800, "y": 670}], "E": -1}
356+ ],
357+ "junctions": [{"x": 300, "y": 560}],
358+ "renderStyle": "standard"
359+ }
360+ },
361+ "id5": {
362+ "Dyad": {
363+ "edges": [
364+ {"S": 1, "M": [], "E": -1},
365+ {"S": -1, "M": [], "E": 2},
366+ {"S": 3, "M": [], "E": -1}
367+ ],
368+ "junctions": [{"x": 350, "y": 700}],
369+ "renderStyle": "standard"
370+ }
371+ },
372+ "id6": {
373+ "Dyad": {
374+ "edges": [
375+ {"S": 1, "M": [], "E": -1},
376+ {"S": -1, "M": [], "E": 2},
377+ {"S": 3, "M": [], "E": -1}
378+ ],
379+ "junctions": [{"x": 650, "y": 700}],
380+ "renderStyle": "standard"
381+ }
382+ },
383+ "id7": {
384+ "Dyad": {
385+ "edges": [
386+ {"S": 1, "M": [], "E": -1},
387+ {"S": -1, "M": [], "E": 2},
388+ {"S": 3, "M": [], "E": -1},
389+ {"S": 4, "M": [], "E": -1},
390+ {"S": 5, "M": [], "E": -1}
391+ ],
392+ "junctions": [{"x": 500, "y": 900}],
393+ "renderStyle": "standard"
394+ }
395+ }
396+ }
397+ }
191398end
0 commit comments