Single Inverted Pendulum¶

First we need the model of the single inverted pendulum. Friedland works it mostly out for us in Chapter 2.

image.png

image.png

image.png

In [1]:
#-----------------------------------------------------------------------------------------------------------
#
#            This code shows how to simulate the nonlinear set of ordinary differential equations in python.
#
#-----------------------------------------------------------------------------------------------------------
import numpy as np
import matplotlib.pyplot as plt
import scipy.integrate as integrate
import matplotlib.animation as animation
from matplotlib.patches import Rectangle
from math import pi, trunc
from numpy import sin, cos, tan

def trim(x, step):
    d = trunc(x / step)
    return step * d

# simulation time
dt = 0.05
Tmax = 5
t = np.arange(0.0, Tmax, dt)  # might need to adjust this to be compatible with the notebook

# set Pendulum Parameters
f = 0.0
# First set up the parameters for the pendulum.  I will use the values for the WWU pendulum as much as I can.
M = 0.3163 # Mass of carriage (kg)
m = 0.0318 # Mass of the long pendulum (kg)
R = 4.09 # Motor resistance (Ohms)
r = 0.0254/2  # Pulley radius (meters)
k = 0.0351 # Torque constant (Nm/A)
K = 0.0351 # Back emf constant (Vs/rad)
l = 0.323 # length of the long pendulum rod (meters)
g = 9.81  # gravity (m/s^2)
s = 1  #  -1 for hanging down; +1 for up
    
# initial conditions
th_dot = .0 		# pendulum angular velocity
th = pi/2.0 + pi	# pendulum angle (start it hanging almost down)
y = .0		# cart position
y_dot = .0		# cart velocity

# y0 = 0		# desired cart position

precision = 0.006 # 
state = np.array([th, th_dot, y, y_dot, trim(th, precision), .0])

def derivatives(state, t):
    ds = np.zeros_like(state)

    _th = state[0]
    _th_dot = state[1]
    _y = state[2]
    _y_dot = state[3]

    ds[0] = state[1]
    ds[1] = ((M+m)*g*tan(_th)-m*l*state[1]**2*sin(_th)-f)/((M+m)*l/cos(_th)-m*l*cos(_th))
    ds[2] = state[3]
    ds[3] = g*tan(_th)-l*ds[1]/cos(_th)

    return ds

print("Integrating...")
# integrate your ODE using scipy.integrate.
solution = integrate.odeint(derivatives, state, t)
print("Done")

ths = solution[:, 0]
th_dots = solution[:,1]
ys = solution[:, 2]
y_dots = solution[:,3]

plt.plot(t, ths, t, th_dots, t, ys, t, y_dots)
plt.legend([r'$\theta$', r'$\dot \theta$', r'$y$', r'$\dot y$'])
plt.grid()
plt.show()
Integrating...
Done

This looks reasonable for hanging down at least.

In [2]:
# This is for the animation of the inverted pendulum nonlinear behavior.

pxs = l * sin(ths) + ys
pys = l * cos(ths)

fig = plt.figure()
ax = fig.add_subplot(111, autoscale_on=False, xlim=(-1.5, 1.5), ylim=(-0.5, 2))
ax.set_aspect('equal')
ax.grid()

patch = ax.add_patch(Rectangle((0, 0), 0, 0, linewidth=1, edgecolor='k', facecolor='g'))

line, = ax.plot([], [], 'o-', lw=2)
time_template = 'time = %.1fs'
time_text = ax.text(0.05, 0.9, '', transform=ax.transAxes)

cart_width = 0.3
cart_height = 0.2

def init():
    line.set_data([], [])
    time_text.set_text('')
    patch.set_xy((-cart_width/2, -cart_height/2))
    patch.set_width(cart_width)
    patch.set_height(cart_height)
    return line, time_text, patch


def animate(i):
    thisx = [ys[i], pxs[i]]
    thisy = [0, pys[i]]

    line.set_data(thisx, thisy)
    time_text.set_text(time_template % (i*dt))
    patch.set_x(ys[i] - cart_width/2)
    return line, time_text, patch

ani = animation.FuncAnimation(fig, animate, np.arange(1, len(solution)),
                              interval=25, blit=True, init_func=init)

# Set up formatting for the movie files
print("Writing video...")
Writer = animation.writers['imagemagick']
writer = Writer(fps=25, metadata=dict(artist='Sergey Royz'), bitrate=1800)
ani.save('controlled-cart.gif', writer=writer)
plt.close()
Writing video...

(The nonlinear state equations, which come from (2E.3) for $\ddot y$ and $\ddot \theta$ are figured out below. ) image.png

image.png

image.png

image.png

This is with the pendulum driven by a force. In problem 2.2 which you did in Homework 2, we have the model when driven by an electric motor, which is what we have with our hardware. Here is the solution from Joe Dinius.

Problem 2.1¶

The cart pendulum equations-of-motion are:

$$ \ddot y + \frac{mg}{M} \theta = \frac{f}{M} \\ \ddot \theta - \frac{M+m}{M \ell} g \theta = -\frac{f}{M \ell} $$

$\tau = rf$, $r$ is the torque-to-linear-force factor. For the electric motor:

$$ \tau = -\frac{k^2}{R} \omega + \frac{K}{R}e \\ = rf \\ \implies f = -\frac{k^2}{Rr} \omega + \frac{K}{Rr}e $$

The linear acceleration, $\ddot y$, is equal to $\ddot y = r \dot \omega \implies \dot y = r \omega$ by Newton's law. This implies: $$ f = -\frac{k^2}{Rr^2} \dot y + \frac{K}{Rr}e $$

Plug in $f$ to the first equation: $$ \ddot y + \frac{mg}{M} \theta = -\frac{k^2}{MRr^2} \dot y + \frac{K}{MRr}e \\ \ddot \theta - \frac{M+m}{M \ell} g \theta = \frac{k^2}{MRr^2 \ell} \dot y - \frac{K}{MRr \ell}e $$

Move state derivatives to the LHS: $$ \ddot y +\frac{k^2}{MRr^2} \dot y + \frac{mg}{M} \theta = \frac{K}{MRr}e \\ \ddot \theta - \frac{k^2}{MRr^2 \ell} \dot y - \frac{M+m}{M \ell} g \theta = - \frac{K}{MRr \ell}e $$

The next step is to take this linearized state model and put it into state form. I am going to put the variable $s$ into the equations where $s=1$ for the pendulum up, and $s= -1$ for the pendulum down.

$$\begin{bmatrix} \dot y \\ \dot \theta \\ \ddot {y} \\ \ddot {\theta} \end{bmatrix} = \begin{bmatrix} 0 && 0 && 1 && 0 \\ 0 && 0 && 0 && 1 \\ 0 && -mg/M && -k^2/(MRr^2)&& 0 \\ 0 && s(M+m)g/(Ml) && sk^2/(MRr^2l) && 0 \end{bmatrix} \begin{bmatrix} y \\ \theta \\ \dot{y} \\ \dot {\theta} \end{bmatrix} + \begin{bmatrix} 0 \\ 0 \\ K/(MRr) \\ -sK/(MRrl) \end{bmatrix} e $$

so $$\mathbf A = \begin{bmatrix} 0 && 0 && 1 && 0 \\ 0 && 0 && 0 && 1 \\ 0 && -mg/M && -k^2/(MRr^2)&& 0 \\ 0 && s(M+m)g/(Ml) && sk^2/(MRr^2l) && 0 \end{bmatrix}$$ and $$\mathbf B = \begin{bmatrix} 0 \\ 0 \\ K/(MRr) \\ -sK/(MRrl) \end{bmatrix}$$

For an ideal motor, the torque constant is equal to the back electromotive force constant, so $k = K$. The applied voltage is $e$. The mass of the cart is $M$; the resistance of the motor is $R$; the radius of the wheels (or pulley in our case) is $r$; and the mass of the pendulum is $m$, with $g$ for gravity. For our pendulum, we might need to re-write these in terms of their moments of inertia sometime.

For now let's assume all the states are available, so $$ \mathbf C = \begin{bmatrix} 1 && 0 && 0 && 0 \\ 0 && 1 && 0 && 0 \\ 0 && 0 && 1 && 0 \\ 0 && 0 && 0 && 1 \end{bmatrix}$$

Let's see if our equations are good by simulating the system to see if it works like we expect.

Unstable Pendulum (Standing up)¶

Let's begin with an investigation when the pendulum is standing up, not hanging down. The model allows this with $s=1$.

In [ ]:
import control as ct

np.set_printoptions(precision=4)

def setupModel(M=0.3163,m=0.0318,R=4.09,r=0.0254/2,k=0.0351,K=k,l=0.323,g=9.81,s=-1):
    # Now the state model
    A = np.array([[0,0,1,0],[0,0,0,1],[0,-m*g/M,-k**2/(M*R*r**2),0],[0,s*(m+M)*g/(M*l),s*k**2/(M*R*r**2*l),0]])
    B = np.transpose(np.array([[0,0,K/(M*R*r),-s*K/(M*R*r*l)]]))
    C = np.eye(4);
    D = 0;
    pendsys = ct.ss(A, B, C, D)
    return pendsys

pendsys = setupModel(M=0.3163,m=0.0318,R=4.09,r=0.0254/2,k=0.0351,K=k,l=0.323,g=9.81,s=1)
print(pendsys)
x0 = [0,0.1,0,0]
t = np.arange(0, 10, 0.01)
(t, y) = ct.initial_response(pendsys,t, x0)

def displayPendSimulation(t, y):
    fig, ax = plt.subplots(figsize=(6, 4), tight_layout=True)
    ax.plot(t, y[0,:])
    ax.plot(t, y[1,:])
    ax.plot(t, y[2,:])
    ax.plot(t, y[3,:])
    ax.legend([r'$y$', r'$\theta$', r'$\dot y$', r'$\dot \theta$'])
    plt.grid()
    
def getABCD(pendsys):
    # %% Getting the model matrices as lists and then as arrays :
    ( A_list , B_list , C_list , D_list ) = ct.ssdata(pendsys)
    A = np.array ( A_list )
    B = np.array ( B_list )
    C = np.array ( C_list )
    D = np.array ( D_list )
    return A, B, C, D
    
def displayEigenStuff(A):
    (w, v) = np.linalg.eig(A)
    print('Eigenvalues of A:', w)
    print('Eigenvectors of A:')
    print(v)
    return v, w
    
A, B, C, D = getABCD(pendsys)
v, w = displayEigenStuff(A)
displayPendSimulation(t, y)
error: the import function is not yet implemented in Octave
error: called from
    import at line 63 column 3
error: 'np' undefined near line 1, column 1
error: 'def' undefined near line 1, column 1
warning: the '**' operator was deprecated in version 7 and will not be allowed in a future version of Octave; please use '^' instead
warning: the '**' operator was deprecated in version 7 and will not be allowed in a future version of Octave; please use '^' instead
warning: the '**' operator was deprecated in version 7 and will not be allowed in a future version of Octave; please use '^' instead
warning: the '**' operator was deprecated in version 7 and will not be allowed in a future version of Octave; please use '^' instead
error: 'np' undefined near line 1, column 9
error: 'np' undefined near line 1, column 9
error: 'np' undefined near line 1, column 9
error: 'ct' undefined near line 1, column 15
error: parse error:

  syntax error

>>>     return pendsys
                     ^
error: 'setupModel' undefined near line 1, column 11
error: 'pendsys' undefined near line 1, column 7
x0 =

        0   0.1000        0        0

error: 'np' undefined near line 1, column 5
error: parse error:

  syntax error

>>> (t, y) = ct.initial_response(pendsys,t, x0)
      ^
error: 'def' undefined near line 1, column 1
error: parse error:

  syntax error

>>>     fig, ax = plt.subplots(figsize=(6, 4), tight_layout=True)
                                         ^
error: parse error:

  syntax error

>>>     ax.plot(t, y[0,:])
                    ^
error: parse error:

  syntax error

>>>     ax.plot(t, y[1,:])
                    ^
error: parse error:

  syntax error

>>>     ax.plot(t, y[2,:])
                    ^
error: parse error:

  syntax error

>>>     ax.plot(t, y[3,:])
                    ^
error: parse error:

  syntax error

>>>     ax.legend([r'$y$', r'$\theta$', r'$\dot y$', r'$\dot \theta$'])
                       ^
error: 'plt' undefined near line 1, column 5
error: 'def' undefined near line 1, column 1
error: parse error:

  syntax error

>>>     ( A_list , B_list , C_list , D_list ) = ct.ssdata(pendsys)
                 ^
error: 'np' undefined near line 1, column 9
error: 'np' undefined near line 1, column 9
error: 'np' undefined near line 1, column 9
error: 'np' undefined near line 1, column 9
error: parse error:

  syntax error

>>>     return A, B, C, D
               ^
error: 'def' undefined near line 1, column 1
error: parse error:

  syntax error

>>>     (w, v) = np.linalg.eig(A)
          ^
error: 'w' undefined near line 1, column 32
error: print: no figure to print
error: called from
    print at line 448 column 5
error: 'v' undefined near line 1, column 11
error: parse error:

  syntax error

>>>     return v, w
               ^
error: 'A' undefined near line 1, column 1
error: 'v' undefined near line 1, column 1
error: 't' undefined near line 1, column 23

Steve Brunton has some interesting things to say about how controllable a system is on his video here. I want to check the single pendulum to see how controllable it is. Steve describes how you can tell what directions are more controllable by doing the singular value decomposition of $\mathbf Q$ because Grammian $\mathbf W_t \approx \mathbf {Q Q}^T$.

In [ ]:
Q = np.hstack([B, np.matmul(A, B), np.matmul(A, np.matmul(A, B)),np.matmul(A, np.matmul(A, np.matmul(A, B)))])
print(Q)
Rank_Q = np.linalg.matrix_rank(Q)
print('Rank of Q is:', Rank_Q)
print('SVD of Q:')
(U,S,V) = np.linalg.svd(Q, full_matrices=True)
print('U :')
print(U)
print('S :', S)
print('V :')
print(V)
error: 'np' undefined near line 1, column 5
error: 'Q' undefined near line 1, column 7
error: 'np' undefined near line 1, column 10
error: 'Rank_Q' undefined near line 1, column 24
error: print: no figure to print
error: called from
    print at line 448 column 5
error: parse error:

  syntax error

>>> (U,S,V) = np.linalg.svd(Q, full_matrices=True)
      ^
error: print: no figure to print
error: called from
    print at line 448 column 5
error: 'U' undefined near line 1, column 7
error: 'S' undefined near line 1, column 14
error: print: no figure to print
error: called from
    print at line 448 column 5
error: 'V' undefined near line 1, column 7

The second eigenvector is the unstable one. Note its direction. Then look at the least controllable direction. Note its direction. We don't want them in the same direction. To find the angle between them, we find the dot or inner product between them. They both appear to be unit vectors the way python calculates them.

In [ ]:
print('The inner product between the most uncontrollable direction and the unstable pole is:')
print(v[:,1])
print(U[:,3])
inner_prod_of_unstable_eigenvector_with_difficult_to_control_direction = np.matmul(np.transpose(v[:,2]),U[:,3])
print(inner_prod_of_unstable_eigenvector_with_difficult_to_control_direction)
print('Just checking that the vectors are normalized when computed by python.  Here is the length of one of them.')
print('The eigenvector\'s length squared is: ')
print(np.matmul(np.transpose(v[:,1]),v[:,1]))
print('The length of U[3] is: ')
print(np.matmul(np.transpose(U[:,3]),U[:,3]))
error: print: no figure to print
error: called from
    print at line 448 column 5
error: parse error:

  syntax error

>>> print(v[:,1])
           ^
error: parse error:

  syntax error

>>> print(U[:,3])
           ^
error: parse error:

  syntax error

>>> inner_prod_of_unstable_eigenvector_with_difficult_to_control_direction = np.matmul(np.transpose(v[:,2]),U[:,3])
                                                                                                     ^
error: 'inner_prod_of_unstable_eigenvector_with_difficult_to_control_direction' undefined near line 1, column 7
error: print: unknown device 
error: called from
    __print_parse_opts__ at line 416 column 5
    print at line 420 column 8
error: parse error:

  syntax error

>>> print('The eigenvector\'s length squared is: ')
                            ^
error: parse error:

  syntax error

>>> print(np.matmul(np.transpose(v[:,1]),v[:,1]))
                                  ^
error: print: no figure to print
error: called from
    print at line 448 column 5
error: parse error:

  syntax error

>>> print(np.matmul(np.transpose(U[:,3]),U[:,3]))
                                  ^

The inner product is small and the length of the vectors that were compared is unity, so that is good! It means they are practically orthogonal. Now let's check to see if we place the poles at the same pole locations we started with, the gain coefficients are all come out very close to zero. This is just a check of our the place algorithm.

In [ ]:
print('If we leave the poles alone, the gain matrix for this state feedback is:')
gain_for_no_pole_movement = ct.place(A, B, w) # Should be 0, 0, 0, 0.
print(gain_for_no_pole_movement)
error: print: no figure to print
error: called from
    print at line 448 column 5
error: 'ct' undefined near line 1, column 29
error: 'gain_for_no_pole_movement' undefined near line 1, column 7

Stable Pendulum Simulation (Hanging Down)¶

In [ ]:
s = -1; # Hanging down
pendsys = setupModel(M=0.3163,m=0.0318,R=4.09,r=0.0254/2,k=0.0351,K=k,l=0.323,g=9.81,s=-1);
print(pendsys)
x0 = [0,0.1,0,0]
(t, y) = ct.initial_response(pendsys,t, x0)
A, B, C, D = getABCD(pendsys)
v, w = displayEigenStuff(A)
displayPendSimulation(t, y)
error: 'setupModel' undefined near line 1, column 11
error: 'pendsys' undefined near line 1, column 7
x0 =

        0   0.1000        0        0

error: parse error:

  syntax error

>>> (t, y) = ct.initial_response(pendsys,t, x0)
      ^
error: 'A' undefined near line 1, column 1
error: 'v' undefined near line 1, column 1
error: 't' undefined near line 1, column 23

Remember this is the result of a simulation of a linear model. The question to ask is, "Is this accurate?" To check this out, remember the approximations we made were $cos \theta \approx 1$ and $sin\theta \approx \theta$. For the largest angles we see this works out to be:

In [ ]:
error_cos = np.cos(0.1)-1
error_sin = np.sin(0.1)-0.1
print('Error in cosine: ', error_cos)
print('Error in sine: ', error_sin)
error: 'np' undefined near line 1, column 13
error: 'np' undefined near line 1, column 13
error: 'error_cos' undefined near line 1, column 28
error: 'error_sin' undefined near line 1, column 26

This isn't the only source of error in our simulation. There are at least a couple others. One is due to not knowing the actual parameters very precisely. An approximate error analysis can be had by looking at the elements in the $A$ matrix to see how they vary as a function of each error. For example if a coefficient, $a_{ij}=f(m,M,r,R,k,K,l)$ then the error is approximately: $$da_{ij} = \frac {\partial a_{ij}} {\partial m} dm+ \frac {\partial a_{ij}} {\partial M} dM+\frac {\partial a_{ij}} {\partial r} dr+\frac {\partial a_{ij}} {\partial R} dR+\frac {\partial a_{ij}} {\partial k} dk+\frac {\partial a_{ij}} {\partial K} dK+\frac {\partial a_{ij}} {\partial l} dl$$

The only reason it damps out is the resistance of the motor windings. We modeled no friction. The underdamped responses (from complex poles) are expected. There could be a damping effect from friction as well. To tell if we should add that to the model, the thing to do is to actually hang the pendulum down, and see if the decay time of the oscillating wave is consistent with what the linear model predicts.

Not changing the pole locations results in a zero gain matrix as expected.

Let's see if we can change the behavior of the pendulum so it is like it was in honey, dieing out in 1, 2, 3, and 4 seconds. We will place the poles at -1, -2, -3 and -4. Then do the simulation.

In [ ]:
G = ct.place(A,B,[-1,-2,-3,-4])
pendsys_fb = ct.ss(np.subtract(A, np.matmul(B, G)), B, C, D);
print(pendsys_fb)
x0 = [0, 0.1, 0, 0]
t, y = ct.initial_response(pendsys_fb, t, x0)
A_fb, B_fb, C_fb, D_fb = getABCD(pendsys_fb)
v, w = displayEigenStuff(A_fb)
displayPendSimulation(t, y)
error: 'ct' undefined near line 1, column 5
error: 'ct' undefined near line 1, column 14
error: 'pendsys_fb' undefined near line 1, column 7
x0 =

        0   0.1000        0        0

error: 't' undefined near line 1, column 1
error: 'A_fb' undefined near line 1, column 1
error: 'v' undefined near line 1, column 1
error: 't' undefined near line 1, column 23

Note how the cart moves to keep the natural swinging behavior from happening. The reason it looks a little oscillatory is because some eigenmodes damp out faster and some slower.

Note that the maximum angle is still $0.1^o$, so our previous check of the angles still applies.

It looks like we were successful placing the poles where we wanted them.

I would like to know how much difference friction is making for the pendulum. The simulation above may seem like a good place to start, but it assumes the input voltage is zero (shorted), but in that configuration, the resistance of the motor windings, $R$, causes damping. So it would be nice to have a model where the current through the motor is zero. This configuration would have no damping due to $i^2R$ losses in the motor.

Zero Current Input Model¶

In this case, the torque $\tau$ is zero, because there is no Lorentz force with zero current. This is as if the force was set to zero in the original pendulum model before incorporating the motor. In this case the state model is: image.png

With the s variable included this becomes:

$$\begin{bmatrix} \dot y \\ \dot \theta \\ \ddot {y} \\ \ddot {\theta} \end{bmatrix} = \begin{bmatrix} 0 && 0 && 1 && 0 \\ 0 && 0 && 0 && 1 \\ 0 && -mg/M && 0 && 0 \\ 0 && s(M+m)g/(Ml) && 0 && 0 \end{bmatrix} \begin{bmatrix} y \\ \theta \\ \dot{y} \\ \dot {\theta} \end{bmatrix} + \begin{bmatrix} 0 \\ 0 \\ 0 \\ 0 \end{bmatrix} u $$

This is like setting $k=K=0$ in the model. Let's see if that does not decay, as we think it should. It should also give us a better measurement of the viscous friction in the real system.

In [ ]:
pendsys_nf  = setupModel(M=0.3163,m=0.0318,R=4.09,r=0.0254/2,k=0,K=0,l=0.323,g=9.81,s=-1)
print(pendsys_nf)
t, y = ct.initial_response(pendsys_nf, t, [0, 0.1, 0, 0])
A_nf, B_nf, C_nf, D_nf = getABCD(pendsys_nf)
v, w = displayEigenStuff(A_nf)
displayPendSimulation(t, y)
error: 'setupModel' undefined near line 1, column 15
error: 'pendsys_nf' undefined near line 1, column 7
error: 't' undefined near line 1, column 1
error: 'A_nf' undefined near line 1, column 1
error: 'v' undefined near line 1, column 1
error: 't' undefined near line 1, column 23

We should check the real pendulum and make sure the pendulum period is $T_p = (2\pi)/5.78144 = 1.0868$ seconds, assuming the damping is negligible. If not, we should see what happens to this frequency as we add damping, and adjust our parameters so the period is correct based on what we think may be amiss.

Model Verification and Adjustment¶

In this section we devise various ways to verify the model parameters and modify the model as seems best. To do this we use the pendulum hanging down using the models for that developed above to verify/adjust and adjust the parameters for $\theta$, and then we put the pendulum on it's side and make a model to verify/adjust the motor parameters.

To do this we need to understand how the WWU pendulum works and how to operate it. This the the WWU inverted pendulum.

image.png

The pendulum motors and shaft angle encoders are controlled by a dedicated digital controller using a Xylinx FPGA that communicates with the user over Ethernet. To control it you use Ralph's code from the ctrlbox.m file below. You have to setup your computer to communicate with the pendulum at address 169.254.0.100. Here are the settings for Ubuntu.

image.png image.png

You can check it works as shown below (on linux). If you can ping the pendulum, you can communicate with it.

image.png

There are two physical pendula. They have slightly different characteristics, and so the constants are different for each.

The pendulum needs to know where down is, and where the center of the track is as well. To set those you hit the reset button on the controller. Sometimes you cannot ping the pendulum, and if you reset it, you can. Here is a photo showing which button is the reset button.

image.png

The ctrlbox.m file is shown below. It contains the octave/MATLAB routines to communicate with the pendulum. Ralph Stirling is the one who wrote them, and he is also the one who wrote the FPGA controller code to communicate with the Wiznet Ethernet device that the controller uses to send the data over Ethernet.

In [ ]:
% ctrlbox.m
%
%    Functions for communication with the inverted pendulum
%    ctrlbox interface via ethernet.
%
% ctrlbox_init - initialize connection to ctrlbox
%
function rval = ctrlbox_init()
    global ctrlbox_con;

    ctrlbox_con = socket();
    sinfo = struct("addr","169.254.0.100", "port", 47820);
    rval = connect(ctrlbox_con,sinfo);

    return;
endfunction
%
% ctrlbox_send(cmdval,enable,period)
%  - send command value, enable, and sample period to ctrlbox
%    - cmdval = -32768 to +32767, where 32767=100% of DC bus voltage
%    - enable = 0 or 1
%    - period in usec
%
%  - future: measure time avg of pwm value, shutoff motor
%    if excessive.
function rval = ctrlbox_send(cmdval,enable,period)
    global ctrlbox_con;

    pwm = min(max(cmdval,-32000),32000);  % This keeps it in the interval (-32000, 32000).
    data = [pwm, 0, enable, period];
    send(ctrlbox_con, typecast(int32(data(1:4)),'uint8'));

    rval = 0;
    return;
endfunction
%
% ctrlbox_recv - receive an array of four values from ctrlbox
%
function data = ctrlbox_recv()
    global ctrlbox_con;

    [rdata,len] = recv(ctrlbox_con,16);

    if (len ~= 16)
        fprintf('short data: %d\n', len);  % Check to make sure we got it all.
    end

    data = double(typecast(rdata,'int32'));
    return;

endfunction

%
% ctrlbox_shutdown - shutdown connection to ctrlbox
%
function ctrlbox_shutdown()
    global ctrlbox_con;

    % turn off motor
    send(ctrlbox_con,typecast(int32([0,0,0,0]),'uint8'));

    disconnect(ctrlbox_con);
endfunction

Now we test the pendulum hanging down to see how it matches our model.

In [ ]:
% hanging_down.m
pkg load sockets control signal;
T=1/100;            % Sample time (1/sample_rate)
Trun= 10;           % Total time to run

% Simulate the system with the model we have
[pendsys,A,B,C,D] = setupModel(M=0.3163,m=0.0318,R=4.09,r=0.0254/2,k=0.0351/1.7,K=k,l=0.242,g=9.81,s=-1);
A
B
C
t = 0:T:Trun;
pendsys_fb = ss(A-B*g,zeros(4,1),eye(4),0);
lsim(pendsys_fb,zeros(size(t)),t,[0;0.1;0;0])  
initial_angle_in_degrees = 0.1*180/pi
[v,D] = eig(A)

% rdata(1) is the long angle (4096 counts per revolution) + is clockwise
% looking at the pendulum so the ribbon cable is on the left.
% rdata(2) is the short pendulum (4096 counts/rev) + clockwise
% rdata(3) is position with positive to the right
% rdata(4) is the CW_POS encoder near where the controlbox plugs in
rd = 0.0254/2;
scale = [2*pi/4096  2*pi/4096 2*pi*rd/4096]  % for receive data
cnt=Trun/T;            % number of times through loop
try
    ctrlbox_init();
    disp('finished ctrlbox_init');
    % send sample period
    period = 1000000.*T;  
    ctrlbox_send(0,0,period);
    ctrlbox_recv();  % The first data point is bad.  This ditches it.
    disp('finished send');
    tic;
        for c=1:cnt
        % read encoder values
        rdata = ctrlbox_recv();

        
        % force matlab to check for interrupts and flush event queue
        drawnow;
           
        % save data
        store(c,:) = [rdata];
    end
    runtime = toc;
    fprintf('transactions=%d seconds=%d transactions/sec=%f\n',
        c, runtime, c/runtime);
    drawnow;
catch
    % if something failed, display error and loop count
    disp(lasterror.message);
end

% disable motor and disconnect
ctrlbox_shutdown();

t=0:T:(cnt-1)*T; 
y = store(1:end,3)*scale(3);
theta = store(1:end,1)*scale(2);  
ydot = diff(y)/T;
thetadot = diff(theta)/T;

figure()
plot(t,theta*180/pi)
title('Pendulum Angle, \theta')
xlabel('Time (s)')
ylabel('(degrees)')
grid

figure()
plot(t(1:end-1),thetadot)
title('Pendulum Angular Velocity, d\theta/dt')
xlabel('Time (s)')
ylabel('(radians/s)')
grid
error: package sockets is not installed
error: called from
    load_packages at line 47 column 7
    pkg at line 632 column 7
error: 'setupModel' undefined near line 1, column 21
error: 'A' undefined near line 1, column 1
error: 'B' undefined near line 1, column 1
error: 'C' undefined near line 1, column 1
error: 'A' undefined near line 1, column 17
error: 'pendsys_fb' undefined near line 1, column 6
initial_angle_in_degrees = 5.7296
error: 'A' undefined near line 1, column 13
scale =

   1.5340e-03   1.5340e-03   1.9482e-05

'socket' undefined near line 4, column 19
error: 'send' undefined near line 5, column 5
error: called from
    ctrlbox_shutdown at line 5 column 5
error: invalid use of 'end': may only be used to index existing value
error: invalid use of 'end': may only be used to index existing value
error: 'theta' undefined near line 1, column 17
error: 'theta' undefined near line 1, column 8
error: 'thetadot' undefined near line 1, column 17


Here is a routine to test the pendulum. This helps us understand how things work. You need to install the octave-sockets package if you don't have it already.

The results from this simulation done with the motor open circuited, so the $R$ has no effect on the slow down of the pendulum seem to indicate that the major contribution to the pendulum friction is not viscous friction. It seems to be a constant torque opposing motion, and it seems to drop off linearly with time instead of exponentially. This paper analyzes this kind of motion. Because it has the term $sgn(\dot \theta)$ in the torque because it changes sign when the angular velocity does, it would require changing models when $\dot \theta$ changes sign. I don't want to mess with that at the moment. I suspect the friction due to the cart moving on the rail is a bigger deal based on just feeling it with my fingers. On that one, I'm not quite sure how to measure it, and I think it will likely also be a force with a $sgn(\dot y)$, I'm going to leave that out of my model too. It may be possible to model those forces as proportional to $\dot \theta$ and $\dot y$, but they would give an exponential decay, and at least for the pendulum, it isn't exponential. However, I do note from the paper that the frequency of oscillation is not a function of the friction, and I also note that the simulation and the measurement are a bit different in the frequency of oscillation. I will take that into account by adjusting the length $l$ to the center of mass of the pendulum. It appears that we measure five periods in about $4.7$ seconds, so the angular frequency is $2 \pi 5/4.7= 6.68$ radians per second. Our simulation has nine periods in about $9.8$ seconds, so that gives an angular frequency of $2 \pi 9/9.8 = 5.7703$ which reminds me we can just get it from the eigenvalue, which was $5.78144$. The characteristic polynomial of the $A$ matrix without the motor connected is $(Mls^4+Mgs^2+gms^2)/(Ml)$. This makes it obvious there are two poles at zero, and the remainder of the poles satisfy $(Mls^2+Mg+gm)=0$ and so the poles are at $\pm j\sqrt{g(1+m/M)/l}$ and this agrees with what octave calculated above. I am not quite sure why, but it seems the of moment of inertia is not quite right, so I am going to reduce the length of the rod to get agreement. The factor for which the resonant frequency is wrong is $6.68/5.78 = \sqrt{l_{old}/l_{new}} = \sqrt{0.323/l_{new}}$ so $l_{new}$ is calculated below as 0.242. The analysis below verifies this change fixes things.

In [ ]:
l = .323/(6.68/5.78)^2
[pendsys_nf, A_nf]  = setupModel(M=0.3163,m=0.0318,R=4.09,r=0.0254/2,k=0,K=0,l,g=9.81,s=-1);
A_nf
[v,D] = eig(A_nf)
lsim(pendsys_nf, zeros(size(t)), t, [0; 0.1; 0; 0])

To start the pendulum, you need to let it hang down until it isn't moving. Then you push the calibrate button on the FPGA board to tell it that is hanging down. Then you set the initial condition and start your program. The pause statement in octave lets you start it when you want with a single keystroke.

Cart Falling Down¶

The force the motor develops is important. We need to verify it, especially as some of the code from previous years seems to indicate that we might not have a good handle on the scale constant for the motor drive.
If $J$ is the moment of inertia of the system: