@@ -206,4 +206,213 @@ metadata {
206206 "id27": {"Dyad": {"edges": [{"S": 1, "M": [{"x": -110, "y": 380}], "E": 2}]}}
207207 }
208208}
209+ end
210+
211+ """
212+ Discrete-time version of `CascadeFFDyadBot`. The cascade-controlled balancing
213+ robot uses sampled-data `DiscretePIDStandard` controllers together with the
214+ same continuous feedforward generator.
215+
216+ Because `DiscretePIDStandard` has no feedforward input, the feedforward
217+ signals are injected with explicit `Add` blocks instead of the `u_ff` ports
218+ used by the continuous `LimPID`. The feedforward generator and selectors run
219+ in continuous time; their outputs are sampled before being combined with the
220+ discrete controller signals. The torque feedforward is added to the inner
221+ controller output, and the tilt-angle feedforward is added to the inner-loop
222+ reference produced by the outer controller. The sample interval is set by the
223+ top-level structural parameter `Ts`.
224+
225+ The feedforward generator matrices are computed with the script
226+ `scripts/compute_feedforward.jl`.
227+ """
228+ component DiscreteCascadeFFDyadBot
229+ world = MultibodyComponents.PlanarMechanics.World(g = 9.82) {^world}
230+ plant = PlanarDyadBot(phi0 = phi0) {^plant}
231+ square = BlockComponents.Sources.Square(amplitude = 0.05, frequency = 1 / 10) {^square}
232+ angle_controller = DiscreteComponents.DiscretePIDStandard(K = 0.487401, Ti = 0.0587352, Td = 0.0420526, Nd = 119.368, y_max = 0.1) {^angle_controller}
233+ pos_controller = DiscreteComponents.DiscretePIDStandard(K = 0.0666576, Ti = 5.25024, Td = 4.81393, Nd = 4.76616, wd = 1, wp = 1, y_max = deg2rad(25.0)) {^pos_controller}
234+ gain = BlockComponents.Math.Gain(k = -1) {^gain}
235+ gain1 = BlockComponents.Math.Gain(k = -1) {^gain1}
236+ """
237+ refgen has 1 input (position reference) and 3 outputs:
238+ y[1]: angle feedforward
239+ y[2]: filtered position reference
240+ y[3]: torque feedforward
241+ """
242+ refgen = BlockComponents.Continuous.StateSpace(nx = ff_nx(), nu = 1, ny = 3, A = ff_A(), B = ff_B(), C = ff_C(), D = ff_D()) {^refgen}
243+ angle_ff = MultibodyComponents.Selector(nu = 3) {^angle_ff}
244+ pos_ref = MultibodyComponents.Selector(nu = 3, index = 2) {^pos_ref}
245+ torque_ff = MultibodyComponents.Selector(nu = 3, index = 3) {^torque_ff}
246+ mux1 = Mux1() {^mux1}
247+ sampler_theta = DiscreteComponents.Sampler() {^sampler_theta}
248+ sampler_x = DiscreteComponents.Sampler() {^sampler_x}
249+ sampler_posref = DiscreteComponents.Sampler() {^sampler_posref}
250+ sampler_angleff = DiscreteComponents.Sampler() {^sampler_angleff}
251+ sampler_torqueff = DiscreteComponents.Sampler() {^sampler_torqueff}
252+ "Adds the sampled tilt-angle feedforward to the inner-loop reference produced by the outer controller."
253+ add_angleref = BlockComponents.Math.Add() {^add_angleref}
254+ "Adds the sampled torque feedforward to the inner controller output before the actuator gain."
255+ add_torque = BlockComponents.Math.Add() {^add_torque}
256+ zoh = DiscreteComponents.ZeroOrderHold() {^zoh}
257+ clock = DiscreteComponents.PeriodicClock(dt = Ts) {^clock}
258+ "Initial tilt angle of the body"
259+ parameter phi0::Angle = 0.1
260+ "Controller sample interval"
261+ structural parameter Ts::Real = 0.005
262+ relations
263+ connect(square.y, mux1.u) {^id10}
264+ connect(mux1.y, refgen.u) {^id11}
265+ connect(refgen.y, pos_ref.u) {^id12}
266+ connect(refgen.y, angle_ff.u) {^id13}
267+ connect(refgen.y, torque_ff.u) {^id14}
268+ connect(pos_ref.y, sampler_posref.u) {^id15}
269+ connect(angle_ff.y, sampler_angleff.u) {^id16}
270+ connect(torque_ff.y, sampler_torqueff.u) {^id17}
271+ connect(plant.x, sampler_x.u) {^id18}
272+ connect(sampler_x.y, pos_controller.measurement) {^id19}
273+ connect(sampler_posref.y, pos_controller.reference) {^id20}
274+ connect(pos_controller.ctr_output, gain1.u) {^id21}
275+ connect(gain1.y, add_angleref.u1) {^id22}
276+ connect(sampler_angleff.y, add_angleref.u2) {^id23}
277+ connect(add_angleref.y, angle_controller.reference) {^id24}
278+ connect(plant.theta, sampler_theta.u) {^id25}
279+ connect(sampler_theta.y, angle_controller.measurement, clock.y) {^id26}
280+ connect(angle_controller.ctr_output, add_torque.u1) {^id27}
281+ connect(sampler_torqueff.y, add_torque.u2) {^id28}
282+ connect(add_torque.y, gain.u) {^id29}
283+ connect(gain.y, zoh.u) {^id30}
284+ connect(zoh.y, plant.torque) {^id31}
285+ metadata {
286+ "Dyad": {
287+ "tests": {"case1": {"stop": 20}},
288+ "icons": {"default": "dyad://DyadBotComponents/PlanarDyadBot.svg"}
289+ },
290+ "_links": {
291+ "world": {
292+ "Dyad": {"placement": {"diagram": {"x1": 150, "y1": 160, "x2": 50, "y2": 60, "rot": 180}}}
293+ },
294+ "plant": {
295+ "Dyad": {"placement": {"diagram": {"x1": 420, "y1": 400, "x2": 620, "y2": 600}}}
296+ },
297+ "square": {
298+ "Dyad": {"placement": {"diagram": {"x1": -830, "y1": 430, "x2": -730, "y2": 530}}}
299+ },
300+ "angle_controller": {
301+ "Dyad": {
302+ "placement": {
303+ "diagram": {"iconName": "default", "x1": 160, "y1": 450, "x2": 260, "y2": 550, "rot": 0}
304+ },
305+ "tags": []
306+ }
307+ },
308+ "pos_controller": {
309+ "Dyad": {
310+ "placement": {
311+ "diagram": {"iconName": "default", "x1": -420, "y1": 450, "x2": -320, "y2": 550, "rot": 0}
312+ },
313+ "tags": []
314+ }
315+ },
316+ "gain": {
317+ "Dyad": {"placement": {"diagram": {"x1": 300, "y1": 460, "x2": 380, "y2": 540}}}
318+ },
319+ "gain1": {
320+ "Dyad": {"placement": {"diagram": {"x1": -260, "y1": 460, "x2": -180, "y2": 540}}}
321+ },
322+ "refgen": {
323+ "Dyad": {"placement": {"diagram": {"x1": -680, "y1": 200, "x2": -560, "y2": 320}}}
324+ },
325+ "angle_ff": {
326+ "Dyad": {"placement": {"diagram": {"x1": -520, "y1": 180, "x2": -440, "y2": 260}}}
327+ },
328+ "pos_ref": {
329+ "Dyad": {"placement": {"diagram": {"x1": -520, "y1": 300, "x2": -440, "y2": 380}}}
330+ },
331+ "torque_ff": {
332+ "Dyad": {"placement": {"diagram": {"x1": -520, "y1": 60, "x2": -440, "y2": 140}}}
333+ },
334+ "mux1": {
335+ "Dyad": {"placement": {"diagram": {"x1": -750, "y1": 430, "x2": -690, "y2": 530}}}
336+ },
337+ "sampler_posref": {
338+ "Dyad": {"placement": {"diagram": {"x1": -440, "y1": 330, "x2": -360, "y2": 410}}}
339+ },
340+ "sampler_angleff": {
341+ "Dyad": {"placement": {"diagram": {"x1": -360, "y1": 180, "x2": -280, "y2": 260}}}
342+ },
343+ "sampler_torqueff": {
344+ "Dyad": {"placement": {"diagram": {"x1": 100, "y1": 60, "x2": 180, "y2": 140}}}
345+ },
346+ "sampler_x": {
347+ "Dyad": {"placement": {"diagram": {"x1": -380, "y1": 630, "x2": -480, "y2": 730}}}
348+ },
349+ "sampler_theta": {
350+ "Dyad": {"placement": {"diagram": {"x1": 320, "y1": 630, "x2": 220, "y2": 730}}}
351+ },
352+ "add_angleref": {
353+ "Dyad": {"placement": {"diagram": {"x1": -160, "y1": 450, "x2": -80, "y2": 530}}}
354+ },
355+ "add_torque": {
356+ "Dyad": {"placement": {"diagram": {"x1": 180, "y1": 450, "x2": 260, "y2": 530}}}
357+ },
358+ "zoh": {
359+ "Dyad": {"placement": {"diagram": {"x1": 400, "y1": 450, "x2": 480, "y2": 550}}}
360+ },
361+ "clock": {
362+ "Dyad": {"placement": {"diagram": {"x1": 200, "y1": 760, "x2": 300, "y2": 860}}}
363+ },
364+ "id10": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
365+ "id11": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
366+ "id12": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
367+ "id13": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
368+ "id14": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
369+ "id15": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
370+ "id16": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
371+ "id17": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
372+ "id18": {
373+ "Dyad": {
374+ "edges": [
375+ {
376+ "S": 1,
377+ "M": [
378+ {"x": 660, "y": 468},
379+ {"x": 660, "y": 780},
380+ {"x": -430, "y": 780},
381+ {"x": -430, "y": 680}
382+ ],
383+ "E": 2
384+ }
385+ ]
386+ }
387+ },
388+ "id19": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
389+ "id20": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
390+ "id21": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
391+ "id22": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
392+ "id23": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
393+ "id24": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
394+ "id25": {
395+ "Dyad": {
396+ "edges": [
397+ {
398+ "S": 1,
399+ "M": [
400+ {"x": 640, "y": 492},
401+ {"x": 640, "y": 720},
402+ {"x": 270, "y": 720},
403+ {"x": 270, "y": 680}
404+ ],
405+ "E": 2
406+ }
407+ ]
408+ }
409+ },
410+ "id26": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
411+ "id27": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
412+ "id28": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
413+ "id29": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
414+ "id30": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
415+ "id31": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
416+ }
417+ }
209418end
0 commit comments