Un piloto automático simplificado

Este módulo introduce un sencillo piloto automático programado en python que ofrece las siguiente caraterísticas:

  • Bucle de control usando Proporcional Integral Derivada (PID)
  • 4 señales de salida utilizando PyBBIO
  • Sensores de entrada,IMU (InvenSense MPU9150), a una frecuencia de 50Hz
  • Enlace Bluetooth para proporcionar un entrada externa

El código se puede encontrar aqui. En las siguientes secciones se abordarán los diferentes módulos necesarios para implementaar un piloto automático sencillo

Motors

Esta clase (código en GitHub) pertime controlar los motores de velocidad mediante modulación de ancho de pulso (PWM).


class Motor:
    """ @brief Class for interfacing with the motors.

        The class controls the motors speed using the sysfs PWM provided
        by Adafruit_BBIO.
        The duty should be provided in the range (0,100) and the speed
        in this range. The hardware is preset to satisfy the robot needs (two motors CW and two CCW)

        @warning in previous versions the range (-100,100) was accepted. New code just accepts
        values in the (0,100).
        @note the hardware is installed in a way that the motors rotating direction is the one needed
        (e.g.: 1,3 CW and 2,4 CCW)
    """


    def __init__(self, motor_number=1, max_speed=100, min_speed=0):
        self.speed = 0;
        # self.motor_pins_list = [["P9_14", "P9_16"],
        #                     ["P8_19", "P8_13"],
        #                     ["P9_22", "P9_21"],
        #                     ["P9_42", "P9_28"]]
        #self.motor_pins_list = ["P9_14", "P9_21","P9_22", "P9_42"]
        self.motor_pins_list = ["P9_16", "P8_13","P9_21", "P9_28"]
`

Los pines complatibles eHRPWM( capaces de una alta resolución PWM). Consulta la hoja de características del procesador para más información.

        if (motor_number > 4) or (motor_number < 1):
            raise Exception("Motor number provided out of bounds! ([1-4])")
        self.motor_number = motor_number # 1, 2, 3 or 4
        self.max_speed= max_speed
        self.min_speed= min_speed

max_speed y min_speed ayudan mientras se esta testeando.

        #self.frequency = 2000
        self.frequency = 943
        self.motor_pin = self.motor_pins_list[self.motor_number - 1]
        #perform PWM initialization
        # DC Brushed motors
        # self.duty_IN1 = 0 # duty input 1 of the motor controller IC
        # self.duty_IN2 = 0
        # PWM.start(self.motor_pin[0], self.duty_IN1, self.frequency)
        # PWM.start(self.motor_pin[1], self.duty_IN2, self.frequency)

La clase esta programada inicialmente para motores brushed y posteriormente fue adaptada para funcionar escobillas.

        # DC Brushless motors
        self.duty = 0
        PWM.start(self.motor_pin, self.duty, self.frequency)

    def setSpeed(self, speed):
        """ @brief Set the duties according to the speed attribute for the DC Brushed motors

            @warning Not to be used with the brushless configuration
        """
        if speed<=self.max_speed or speed>=self.min_speed:
            if speed > 0:
                self.duty_IN1 = abs(speed)
                self.duty_IN2 = 0
            else:
                self.duty_IN1 = 0
                self.duty_IN2 = abs(speed)
        else:
            raise Exception("Speed provided not in the [0,100] range!")

    def setSpeedBrushless(self, speed):
        """ @brief Set the duties according to the speed attribute for the DC Brushless motors
        """
        if speed<=self.max_speed or speed>=self.min_speed:
            self.duty = speed
        else:
            raise Exception("Speed provided not in the [0,100] range!")

después de ajustar la velocidad deseada con setSpeedBrushless ( el método brushed esta obsoleto), el método go envía la acción cada ciclo al submodulo eHRPWM.


    def go(self):
        """ @brief update the motor PWM according to the class duty attributes
        """
        # DC Brushed motors
        # PWM.set_duty_cycle(self.motor_pin[0],self.duty_IN1)
        # PWM.set_duty_cycle(self.motor_pin[1],self.duty_IN2)
        # DC Brushless motors
        PWM.set_duty_cycle(self.motor_pin, self.duty)

IMU

La siguiente clase(código en GitHub) esta programada para el sensor InvenSense MPU9150. El código hace uso del controlador del sensor en Linux, compilado como una libreria compartida (libimu.so) a través de python Ctypes:

# array class
Vector3d_t = 3*c_float
# array class
Quaternion_t = 4*c_float

# struct with C types
class Mpudata_t(Structure):
         _fields_ = [("rawGyro", c_short*3),
                     ("rawAccel", c_short*3),
                     ("rawQuat", c_long*4),
                     ("dmpTimestamp", c_ulong),
                     ("rawMag", c_short*4),
                     ("magTimestamp", c_ulong),
                     ("calibratedAccel", c_short*4),
                     ("calibratedMag", c_short*4),
                     ("fusedQuat", 4*c_float),
                     ("fusedEuler", 3*c_float),
                     ("lastDMPYaw", c_float),
                     ("lastYaw", c_float)
                    ]

Compatible con las definiciones de C-types.


class IMU:
    """ Interface with the Inertial Measurement Unit.

    The IMU consists of an InvenSense 9-axis MPU-9150. This sensor provides
    readings from 3 accelerometers, 3 magnetometers and 3 gyroscopes.
    Furthermore, the module has a DMP (Digital Motion Processor) integrated
    that makes the calculations necessary to provide filtered outputs.
    """
    def __init__(self):
        #TODO Set I2C interface, make sure that calibrations files are available and make some readings
        # through ctypes.
        self.lib = CDLL("/root/erle_control/imu/libimu.so")
        self.i2c_bus = 2
        self.lib.mpu9150_set_debug(0) # 1
        self.sample_rate = 50 # 50 Hz

Se esta utilizando el sensor DMP porque lo que es lo más rápido que podemos funcionar.

        self.yaw_mix_factor = 3

        # initialize the IMU
        res = self.lib.mpu9150_init(self.i2c_bus, self.sample_rate, self.yaw_mix_factor)
        if res:
            Exception("Error when initializing the IMU!")

El archico de calibración debería haber sido generado previamente. Comprueba el código del driver para más información.

        # set calibration files
        res = self.lib.set_cal(0, "./imu/accelcal.txt")
        if res != 0:
            Exception("Error while calibration: accelcal.txt")
        res = self.lib.set_cal(1, "./imu/magcal.txt")
        if res != 0:
            Exception("Error while calibration: magcal.txt")

Los siguiente métodos reciben los datos de las funciones en C. Esto se hace para que el código pueda ser escrito en python pero usando el controlador escrito en C.


    """ Reads the raw gyro data from the sensor.
            pass a "timing = 1" parameter to measure the time for the measurement.
        @return  gyroX, gyroY, gyroZ
    """
    def read_rawGyro(self, timing = 0):
        if timing:
            start = clock()
        while 1:
                # Parameters to be passed by reference
                x = c_short(0)
                y = c_short(0)
                z = c_short(0)
                function = self.lib.read_rawGyro
                function.argtypes = [POINTER(c_float), POINTER(c_float), POINTER(c_float)]
                res = function(byref(x), byref(y), byref(z))
                if res == 0:
                        time_s = clock() - start
                        print time_s
                        return x.value, y.value, z.value

    """ Reads fused euler angles
            pass a "timing = 1" parameter to measure the time for the measurement.
        @return  eulerX, eulerY, eulerZ (degrees)
    """
    def read_fusedEuler(self, timing = 0):
        if timing:
            start = clock()

        # DMP fused euler angles
        fusedX = c_float(0)
        fusedY = c_float(0)
        fusedZ = c_float(0)
        function = self.lib.read_fusedEuler
        function.argtypes = [POINTER(c_float), POINTER(c_float), POINTER(c_float)]
        while 1:
                res = function(byref(fusedX), byref(fusedY), byref(fusedZ))
                # if timing:
                #     time_s = clock() - start
                #     print "before res:"+str(time_s)
                if res == 0:
                        if timing:
                            time_s = clock() - start
                            print time_s
                        return fusedX.value, fusedY.value, fusedZ.value

                # if the measurement is not ready yet, wait the sampling freq
                sleep(1./self.sample_rate)


    """ Reads all the IMU sensor information and stores it into a Mpudata_t.
            pass a "timing = 1" parameter to measure the time for the measurement.

            TODO: Eventually substitute this way of getting data for a ctypes direct cast
    """
    def read_mpudata_t(self, timing = 0):
        if timing:
            start = clock()
        while 1:
                # Parameters to be passed by reference
                # Raw gyro values
                gyroX = c_short(0)
                gyroY = c_short(0)
                gyroZ = c_short(0)
                # Raw accel values
                accelX = c_short(0)
                accelY = c_short(0)
                accelZ = c_short(0)
                # Raw quaternion values
                quat1 = c_long(0)
                quat2 = c_long(0)
                quat3 = c_long(0)
                quat4 = c_long(0)
                # DMP timestamp
                dmpTimestamp = c_ulong(0)
                # Raw accel values
                magX = c_short(0)
                magY = c_short(0)
                magZ = c_short(0)
                # magnetometer timestamp
                magTimestamp = c_ulong(0)
                # Calibrated accelerometer values
                calibratedAccelX = c_short(0)
                calibratedAccelY = c_short(0)
                calibratedAccelZ = c_short(0)
                # Calibrated magnetometer values
                calibratedMagX = c_short(0)
                calibratedMagY = c_short(0)
                calibratedMagZ = c_short(0)
                # DMP fused quaternions
                fusedQuat1 = c_float(0)
                fusedQuat2 = c_float(0)
                fusedQuat3 = c_float(0)
                fusedQuat4 = c_float(0)
                # DMP fused euler angles
                fusedX = c_float(0)
                fusedY = c_float(0)
                fusedZ = c_float(0)
                # Last DMP Yaw
                lastDMPYaw = c_float(0)
                # Last Yaw
                lastYaw = c_float(0)

                function = self.lib.read_mpudata_t
                function.argtypes = [POINTER(c_short), POINTER(c_short), POINTER(c_short),
                                    POINTER(c_short), POINTER(c_short), POINTER(c_short),
                                    POINTER(c_long), POINTER(c_long), POINTER(c_long), POINTER(c_long),
                                    POINTER(c_ulong),POINTER(c_short), POINTER(c_short), POINTER(c_short),
                                    POINTER(c_ulong),POINTER(c_short), POINTER(c_short), POINTER(c_short),
                                    POINTER(c_short), POINTER(c_short), POINTER(c_short),
                                    POINTER(c_float), POINTER(c_float), POINTER(c_float), POINTER(c_float),
                                    POINTER(c_float), POINTER(c_float), POINTER(c_float),
                                    POINTER(c_float), POINTER(c_float)]

                res = function(byref(gyroX), byref(gyroY), byref(gyroZ),
                                byref(accelX), byref(accelY), byref(accelZ),
                                byref(quat1), byref(quat2), byref(quat3), byref(quat4),
                                byref(dmpTimestamp), byref(magX), byref(magY), byref(magZ),
                                byref(magTimestamp), byref(calibratedAccelX), byref(calibratedAccelY), byref(calibratedAccelZ),
                                byref(calibratedMagX), byref(calibratedMagY), byref(calibratedMagZ),
                                byref(fusedQuat1), byref(fusedQuat2), byref(fusedQuat3), byref(fusedQuat4),
                                byref(fusedX), byref(fusedY), byref(fusedZ), byref(lastDMPYaw), byref(lastYaw))

                if res == 0:
                        if timing:
                            time_s = clock() - start
                            print time_s
                        # Construct an instance of Mpudata_t
                        mpudata_t = Mpudata_t(rawGyro = (c_short*3)(*[gyroX.value, gyroY.value, gyroZ.value]),
                                                rawAccel = (c_short*3)(*[accelX.value, accelY.value, accelZ.value]),
                                                rawQuat = (c_long*3)(*[quat1.value, quat2.value, quat3.value,quat4.value]),
                                                dmpTimestamp = (c_ulong)(dmpTimestamp.value),
                                                rawMag = (c_short*3)(*[magX.value, magY.value, magZ.value]),
                                                magTimestamp = (c_ulong)(magTimestamp.value),
                                                calibratedAccel = (c_float*3)(*[calibratedAccelX.value, calibratedAccelY.value, calibratedAccelZ.value]),
                                                calibratedMag = (c_float*3)(*[calibratedMagX.value, calibratedMagY.value, calibratedMagZ.value]),
                                                fusedQuat = (c_float*4)(*[fusedQuat1.value, fusedQuat2.value, fusedQuat3.value, fusedQuat4.value]),
                                                fusedEuler = (c_float*3)(*[fusedX.value, fusedY.value, fusedZ.value]),
                                                lastDMPYaw = (c_float)(lastDMPYaw),
                                                lastYaw = (c_float)(lastYaw)

                                            )
                        return mpudata_t

PID

Esta clase (código en GitHub) implementa un sencillo algoritmo de control PID. Las ganancias K_p (proporcional), K_d (derivada) and K_i (integral) se pueden pasar como parámetros al inicializar la clases.

class PID:
    """ Simple PID control.

        This class implements a simplistic PID control algorithm. When first
        instantiated all the gain variables are set to zero, so calling
        the method GenOut will just return zero.
    """
    def __init__(self, Kp = 1, Kd = 0, Ki = 0):
        # initialze gains
        self.Kp = Kp
        self.Kd = Kd
        self.Ki = Ki

        self.Initialize()

Setters para las ganancias.

    def SetKp(self, invar):
        """ Set proportional gain. """
        self.Kp = invar

    def SetKi(self, invar):
        """ Set integral gain. """
        self.Ki = invar

    def SetKd(self, invar):
        """ Set derivative gain. """
        self.Kd = invar

    def SetPrevErr(self, preverr):
        """ Set previous error value. """
        self.prev_err = preverr

    def Initialize(self):
        # initialize delta t variables
        self.currtm = time.time()
        self.prevtm = self.currtm

        self.prev_err = 0

        # term result variables
        self.Cp = 0
        self.Ci = 0
        self.Cd = 0

Un flag para la depuración es incluido. Si esta a true, el método update de volvera verbose

    def update(self, error, debug = 0):
        """ Performs a PID computation and returns a control value based on
            the elapsed time (dt) and the error signal from a summing junction
            (the error parameter).
        """
        if debug:
            print "   ****************************"
            print "   PID Debug"
            print "   ****************************"
            print "   error:"+str(error)

        self.currtm = time.time()               # get t
        dt = self.currtm - self.prevtm          # get delta t
        if debug:
            print "   dt:"+str(dt)
        de = error - self.prev_err              # get delta error
        if debug:
            print "   de:"+str(de)

        self.Cp = error               # proportional term
        if debug:
            print "   Proportional term (Cp*Kp):"+str(self.Cp)
        self.Ci += error * dt                   # integral term
        if debug:
            print "   Integral term (Ci*Ki):"+str(self.Ci * self.Ki)

        self.Cd = 0
        if dt > 0:                              # no div by zero
            self.Cd = de/dt                     # derivative term
            if debug:
                print "   Derivative term (Cd*Kd):"+str(self.Cd * self.Kd)

        self.prevtm = self.currtm               # save t for next pass
        self.prev_err = error                   # save t-1 error

        # sum the terms and return the result
        terms_sum = self.Cp * self.Kp  + (self.Ki * self.Ci) + (self.Kd * self.Cd)

terms_sum implementa la ecuación del control PID.

\begin{equation}u(t) = K_p \cdot e(t) + K_i \int_0^t e(\tau) d\tau + K_d \frac{de(t)}{dt}\end{equation}

        if debug:
            print "   Terms sum (self.Cp + (self.Ki * self.Ci) + (self.Kd * self.Cd)):"+str(terms_sum)
            print "   ****************************"
        return terms_sum

PID Calibration

Intuition

Respuesta CL Tiempo de ascenso OVERSHOOT Tiempo de establecimiento S-S ERROR
Kp Reducir Incrementar Pequeño cambio Reducir
Ki Reducir Incrementar Increase Eliminar
Kd Pequeño cambio Reducir Decrease No cambiar

Los quadricopteros son simetricos, por lo tanto se pueden establecer los mismo valores del PID para el pitch y roll. El valor de desvío no es tan importante como los de pitch y roll. Así que para comenzar se establecen los mismos valores para pitch y roll (aunque tal vez no sea lo mejor). Después de que su drone este relativamente estable, puede empezar a alterar las ganancias Yaw. Para drones no simétricos como Hexacopteros o Tricopteros, es posible que afinar el pitch y roll por separado, después de haber tenido un poco de experiencia de vuelo.

Ajuste de la P

Para la ganancia P, empezar poco a poco, hasta que se note que se esta produciendo la oscilación. El ajuste fino se realiza hasta que el movimiento no sea lento ni oscilaciones.

Ajuste de la I

Para la ganancia I, de nuevo empezar despacio e ir increcementado lentamente. Hay que prestar atención al tiempo que necesita para detenerse y estabilizarse. Se quiere llegar a un punto donde se estabilice muy rápido y que no se pase mucho tiempo dando vueltas. Para obtener un mejor valor de I se puede poner a prueba bajo condiciones de viento.

Ajuste de la D

Para la ganancia D, puede entrar en una complicada interacción con los valores P e I. Cuando se estabiliza la ganancia D, es necesario volver atrás y afinar P para mantener bien estabilizada la planta.

Modelo dinámico

Esta clase (código en GitHub) implementa el modelo dinámico del cuadricoptero:


class Dynamical_Model:
    def __init__(self):
        # general paramters
        self.g = 9.806 # Gravity constant [m s^-2 ]
        self.rho = 1.293 # Air density [kg m^-3]
        self.nu = 1.8e-5 # Air viscosity at 20 degrees Celsius [Pa s]

        # quadrotor parameters
        self.P = 4 # number of propellers
        self.L = 29.9974e-3 # Arm length [m]
        self.Vol = 0.00281784516 # Volume [m3] (((86.36*86.36)/1000) *
        self.m = 60e-3 # quadrotor mass [kg]
        self.h = 17e-3 # Vertical distance between CoG and propellers plan [m]                         #                  (8*29.99/1000) * (1.5748/1000))
        self.b = 3.13e-5 # thrust factor in hover [N s^2]
        self.d = 7.5e-7 # drag factor in hover [N m s^2]
        self.W_prop = (self.m * self.g)/self.P # weight of the quadrotor per propeller [N]
        self.Omega_H = math.sqrt(self.W_prop/self.b) # propeller speed at hover

        # propellers
        self.N = 3 # Number of blades per propeller
        self.R = 32.5e-3 # Propeller radius [m]
        self.A = math.pi*math.pow(self.R, 2)# Propeller disk area [m^2]
        self.c = 0.0394 # Chord [m]
        self.theta_0 = 0.2618 # Pitch of incidence [rad]
        self.theta_tw = 0.045 # Twist pitch [rad]
        self.sigma = self.N * self.c/(math.pi * self.R) # Solidity ratio (rotor fill ratio) [rad^-1]
        self.a = 5.7 # Lift slope
        self.C_d = 0.052 # Airfoil drag coefficient
        self.A_c = 0.005 # Helicopter center hub area [m^2]
        # Longitudinal drag coefficients
        self.Cx = 1.32
        self.Cy = 1.32
        self.Cz = 1.32

        # Inertia components [kg m^2]
        self.Ixx = 6.228e-3
        self.Iyy = 6.228e-3
        self.Izz = 1.121e-2

        # motor parameters
        # TODO complete the motor parameters
        self.k_m = 1 #TODO # torque constant
        self.tau = 1 #TODO # motor time constant
        self.eta = 1 #TODO # motor efficiency
        self.Omega_0 = 1 #TODO # point of linearization of the rotor speeds

        self.r = 4 # Reduction ratio
        self.J_t = 6.0100e-5 # Rotor inertia [kg m^2]

        # matrix for calculating the motor voltages from the control inputs
        self.m = np.matrix( ((1/(4*self.b),0, 1/(2*self.b), -1/(4*self.b)),
                        (1/(4*self.b),-1/(2*self.b), 0, 1/(4*self.b)),
                        (1/(4*self.b),0, -1/(2*self.b), -1/(4*self.b)),
                        (1/(4*self.b),1/(2*self.b), 0 ,  1/(4*self.b))) )



    """ Compute the motor voltages from the control inputs following M.Wieremma MS thesis (MSc_thesis_X-UFO). Keep in mind when
        passing parameters the following correspondences.
            - U1: thrust
            - U2: roll
            - U3: pitch
            - U4: yaw

        @returns: u=[u_m1, u_m2, u_m3, u_m3], motor voltages
    """
    def motor_inversion1(self, thrust, roll, pitch, yaw, logging = 0):
        # the control inputs
        U = np.array( ((thrust, roll, pitch, yaw)) )
        Um = np.matrix(U).T
        # the motor voltages
        u = (self.k_m * self.tau) * ((1/self.tau + 2*self.d*self.Omega_0/(self.eta*np.power(self.r,3)*self.J_t))\
            * np.sqrt(np.dot(self.m,U))- self.d*np.power(self.Omega_0,3)/(self.eta*np.power(self.r,3)*self.J_t))

        # u comes in the form [[ 351.0911185   117.65355114  286.29403363  nan]] where nan denotes that this value
        # should be put to 0
        # values goes more or less up to 1500 so they are divided by 15 so that they fall in the 0-100 range.

        if math.isnan(u[0,0]):
            motorPowerM1 = 0
        else:
            motorPowerM1 = u[0,0]/15

        if math.isnan(u[0,1]):
            motorPowerM2 = 0
        else:
            motorPowerM2 = u[0,1]/15

        if math.isnan(u[0,2]):
            motorPowerM3 = 0
        else:
            motorPowerM3 = u[0,2]/15

        if math.isnan(u[0,3]):
            motorPowerM4 = 0
        else:
            motorPowerM4 = u[0,3]/15

        if logging:
            #Log the motor powers:
            print "------------------------"
            print "motorPowerM1 (method 1):" + str(motorPowerM1)
            print "motorPowerM2 (method 1):" + str(motorPowerM2)
            print "motorPowerM3 (method 1):" + str(motorPowerM3)
            print "motorPowerM4 (method 1):" + str(motorPowerM4)
            print "**************************"

        ur = [motorPowerM1, motorPowerM2, motorPowerM3, motorPowerM4] # to be limited
        return ur

    """ Compute the motor voltages from the control inputs
        following bitcraze Crazyflie implementation.

        @returns: u=[u_m1, u_m2, u_m3, u_m3], motor voltages
    """
    def motor_inversion2(self, thrust, roll, pitch, yaw, logging = 0):
        #QUAD_FORMATION_NORMAL
        motorPowerM1 = thrust + pitch + yaw
        motorPowerM2 = thrust - roll - yaw
        motorPowerM3 = thrust - pitch + yaw
        motorPowerM4 = thrust + roll - yaw

        if logging:
            #Log the motor powers:
            print "------------------------"
            print "motorPowerM1 (method 2):" + str(motorPowerM1)
            print "motorPowerM2 (method 2):" + str(motorPowerM2)
            print "motorPowerM3 (method 2):" + str(motorPowerM3)
            print "motorPowerM4 (method 2):" + str(motorPowerM4)
            print "**************************"

        ur = [motorPowerM1, motorPowerM2, motorPowerM3, motorPowerM4] # to be limited
        return ur


    """ Compute the motor voltages from the control inputs
        using a HACKED version of the implementation used in the crazyflie.
        This hack is because the reference frames of Erle are different from the ones
        adopted by the crazyflie.

        M1 <-> M3
        M2 <-> M4

        @returns: u=[u_m1, u_m2, u_m3, u_m3], motor voltages
    """
    def motor_inversion3(self, thrust, roll, pitch, yaw, logging = 0):
        #QUAD_FORMATION_NORMAL
        motorPowerM3 = thrust + pitch + yaw
        motorPowerM4 = thrust - roll - yaw
        motorPowerM1 = thrust - pitch + yaw
        motorPowerM2 = thrust + roll - yaw

        if logging:
            #Log the motor powers:
            print "------------------------"
            print "motorPowerM1 (method 3):" + str(motorPowerM1)
            print "motorPowerM2 (method 3):" + str(motorPowerM2)
            print "motorPowerM3 (method 3):" + str(motorPowerM3)
            print "motorPowerM4 (method 3):" + str(motorPowerM4)
            print "**************************"

        ur = [motorPowerM1, motorPowerM2, motorPowerM3, motorPowerM4] # to be limited
        return ur

    """ Compute the motor voltages from the control inputs following M.Wieremma MS thesis (MSc_thesis_X-UFO).

        THE DYNAMICAL MODEL DOCUMENTED IN THE MS Thesis HAS BEEN HACKED (M1 <-> M3, M2 <-> M4) BECAUSE THE REFERENCE FRAMES ADOPTED
        IN THE DOCUMENT INDICATES THAT THE MOTORS ROTATE IN OPPOSITE DIRECTIONS.

        Keep in mind when passing parameters the following correspondences.

            - U1: thrust
            - U2: roll
            - U3: pitch
            - U4: yaw

        @returns: u=[u_m1, u_m2, u_m3, u_m3], motor voltages
    """
    def motor_inversion4(self, thrust, roll, pitch, yaw, logging = 0):
        # the control inputs
        U = np.array( ((thrust, roll, pitch, yaw)) )
        Um = np.matrix(U).T
        # the motor voltages
        u = (self.k_m * self.tau) * ((1/self.tau + 2*self.d*self.Omega_0/(self.eta*np.power(self.r,3)*self.J_t))\
            * np.sqrt(np.dot(self.m,U))- self.d*np.power(self.Omega_0,3)/(self.eta*np.power(self.r,3)*self.J_t))

        # u comes in the form [[ 351.0911185   117.65355114  286.29403363  nan]] where nan denotes that this value
        # should be put to 0
        # values goes more or less up to 1500 so they are divided by 15 so that they fall in the 0-100 range.

        # FIXED APPLIED DUE TO THE DIFFERENCE IN THE REFERENCE FRAMES

        if math.isnan(u[0,0]):
            motorPowerM3 = 0
        else:
            motorPowerM3 = u[0,0]/15

        if math.isnan(u[0,1]):
            motorPowerM4 = 0
        else:
            motorPowerM4 = u[0,1]/15

        if math.isnan(u[0,2]):
            motorPowerM1 = 0
        else:
            motorPowerM1 = u[0,2]/15

        if math.isnan(u[0,3]):
            motorPowerM2 = 0
        else:
            motorPowerM2 = u[0,3]/15

        if logging:
            #Log the motor powers:
            print "------------------------"
            print "motorPowerM1 (method 4):" + str(motorPowerM1)
            print "motorPowerM2 (method 4):" + str(motorPowerM2)
            print "motorPowerM3 (method 4):" + str(motorPowerM3)
            print "motorPowerM4 (method 4):" + str(motorPowerM4)
            print "**************************"

        ur = [motorPowerM1, motorPowerM2, motorPowerM3, motorPowerM4] # to be limited
        return ur

Controlador Bluetooth

Esta clase (Código en GitHub) crea un servidor bluetooth que espera conexiones entrantes. Pertime al piloto automático recivir la entradas desde el exterior (Por ejemplo usando una aplicación Android)

class BT_Controller:
  """
  """

  def __init__(self, thrust_d = 0, pitch_d = 0, roll_d = 0, yaw_d = 0):
    self.thrust_d = thrust_d
    self.pitch_d = pitch_d
    self.roll_d = roll_d
    self.yaw_d = yaw_d
    self.t = threading.Thread(target=self.server, args = (self.thrust_d,))

  def getThrust(self):
    return self.thrust_d

  def run(self):
    self.t.daemon = True
    self.t.start()
    # t.join()

  def stop(self):
    self.t.exit()

  def server(self, thrust):
    """
    """
    while True:
      server_sock=BluetoothSocket( RFCOMM )
      server_sock.bind(("",PORT_ANY))
      server_sock.listen(2)

      port = server_sock.getsockname()[1]

      uuid = "00001101-0000-1000-8000-00805F9B34FB"

      advertise_service( server_sock, "SampleServer",
                         service_id = uuid,
                         service_classes = [ uuid, SERIAL_PORT_CLASS ],
                         profiles = [ SERIAL_PORT_PROFILE ],
    #                     protocols = [ OBEX_UUID ]
                          )

      print("Waiting for connection on RFCOMM channel %d" % port)

      client_sock, client_info = server_sock.accept()
      print("Accepted connection from ", client_info)

      try:
          while True:
              data = client_sock.recv(1024)
              if len(data) == 0: break
              firstByte = data[0]
              firstByte_hexlify = binascii.hexlify(data[0])

              if firstByte == "U":
                """
                The received data follows the following pattern:
                55 [U]
                01 [left joystick "intensity"]
                05 [left joystick "angle"]
                00 [right joystick "intensity"]
                00 [right joystick "angle"]
                """
                # print "U received"
                # print "thrust: "+str(int(binascii.hexlify(data[1]),16)*10)
                self.thrust_d = int(binascii.hexlify(data[1]),16)*10
              elif firstByte == "T":
                print "T received"
              elif firstByte == "A":
                print "A received"
              elif firstByte == "B":
                print "B received"
              elif firstByte == "C":
                print "C received"
              elif firstByte == "D":
                print "D received"
              else:
                # print data
                print "-----not recognized-----"
                for d in data:
                  print binascii.hexlify(d)

      except IOError:
          #pass
          print("disconnected")
          client_sock.close()
          server_sock.close()

Por ahora solo las entradas que se usan están cableadas.

Estabilización

La siguiente clase (código en GitHub) reúne todos los elementos anteriores y ejecuta el bucle de estabilización a 50 Hz.

import os
from subprocess import call
from imu import IMU
from motors import Motor
from pid import PID
from time import sleep
import datetime as dt
from dynamical_model import Dynamical_Model
import signal
import sys
from bt_controller import BT_Controller

"""
When testing is important to power off the motors when the
script is stoped through SIGINT
"""
def signal_handler(signal, frame):
        print 'Setting all motors to 0...'
        motor1.setSpeedBrushless(0)
        motor2.setSpeedBrushless(0)
        motor3.setSpeedBrushless(0)
        motor4.setSpeedBrushless(0)

        for mot in motors:
            mot.go()

        # calculate the frequency of the main loop
        sum = 0
        for f in frequencies:
            sum+=f
        print "average frequency (Hz): "+str(sum/len(frequencies))
        print "minimum frequency (Hz): "+str(min(frequencies))

        print "killing the controller thread (bt thread)..."
        # # stop bt-controller NOT WORKING
        # bt.stop()

        sys.exit(0)

Este controlador esta programado para que cuando se prescione Ctrl+C detenga el script de estabilización, el robot no continua funcionando (o incluso se vuelve loco).El controlador, establece la velocidad de motores a 0 y da salida a una medida de la velocidad de actualización de la IMU.


""" Limits the thrust passed to the motors
    in the range (-100,100)
"""
def limitThrust(thrust, upperLimit = 100, lowerLimit = 0):
    if thrust > upperLimit:
        thrust = upperLimit
    elif thrust < lowerLimit:
        thrust = lowerLimit
    return thrust

Esta función permite limitar la potencia. Es util para pruebas. Asegurate de utilizarlo si lo utilizas en casa y ten cuidado con las lamparas.


###############################
#    INIT
###############################
# Set the handler for SIGINT
signal.signal(signal.SIGINT, signal_handler)

# Activate I2C-2
retvalue = os.system("/bin/echo BB-I2C1 > $SLOTS")

# Activate the motors with the init script
retvalue = os.system("/usr/bin/python /root/erle_control/init_motors.py")

# First time the IMU sensor raises an error (probably an error with the firmware or so). The following program readies the sensor to work properly:
#retvalue = os.system("/root/erle_control/imu/imu -b 2")
call(["/root/erle_control/imu/imu", "-b2"])


############################
# variables
############################

logging = 0
limit_thrust = 70

roll_d = 0
pitch_d = 0
yaw_d = 0
z_d = 0
#xpos = 0
#ypos = 0

# attitude constants
if len(sys.argv) > 1:
    Kp = float(sys.argv[1])
    Kd = float(sys.argv[2])
    Ki = float(sys.argv[3])

else:
    Kp = 0.9
    Kd = 0.2
    Ki = 0

############################

#instantiate the BT controller
bt = BT_Controller(z_d)
bt.run()

#instantiate IMU
#TODO see how to import C interface to python
imu=IMU()
#MyKalman=KalmanFilter(....)

# dynamical model instance
dyn_model = Dynamical_Model()

#instantiate motors and put them together in a list
motor1=Motor(1)
motor2=Motor(2)
motor3=Motor(3)
motor4=Motor(4)
motors=[motor1,motor2,motor3,motor4]

# instantiate PID controllers
rollPID=PID(Kp, Kd, Ki) # Kp, Kd, Ki
pitchPID=PID(Kp, Kd, Ki)
yawPID=PID(0, 0, 0)
zPID=PID(1, 0, 0)
#xposPID=PID(-0.09, -0.1, 0)
#yposPID=PID(-0.09, -0.1, 0)

# test variables
frequencies = []

if logging:
    print "------------------------"
    print "     stabilize loop     "
    print "------------------------"
############################
#loop
############################

This is the main loop. Data is acquired from the IMU (using the DMP), the PIDs are updated and the output is forwarded to the motors.

while 1:
    start = dt.datetime.now()

    # process the input from the controller
    z_d = bt.getThrust()
    # print "z_d: "+str(z_d)
    # pitch, roll and yaw DESIRED, get FROM THE TELEOPERATOR (for now, global vars are used)
    # roll_d = 0
    # pitch_d = 0
    # yaw_d = 0
    # z_d = 10
    # #xpos = 0
    # #ypos = 0

    #Measure angles
    #roll_m, pitch_m, yaw_m = imu.read_fusedEuler()
    roll_m, pitch_m, yaw_m = imu.read_fusedEuler(0)
    #MyKalman.measure([roll,pitch, yaw])

    z_m = 0 # putting this is the same as telling it to always increase the thrust (go up)

    #Run the PIDs
    roll = rollPID.update(roll_d - roll_m, 0)
    pitch = pitchPID.update(pitch_d - pitch_m, 0)
    yaw = yawPID.update(yaw_d - yaw_m, 0)
    z = zPID.update(z_d - z_m, 0)
    #xpos = xposPID.update(xpos_d - xpos_m)
    #ypos = yposPID.update(ypos_d - ypos_m)

    #TODO change this parameter and see the behaviour
    #thrust is provided by the controller (NOTE: this is also treated as "z" and it should use the zPID controller)
    # the point of hovering is 35% duty cycle in the motors


    if logging:
        #Log the values:
        print "**************************"
        print "Desired angles:"
        print "     pitch:" + str(pitch_d)
        print "     roll:" + str(roll_d)
        print "     yaw:" + str(yaw_d)
        print "     z:" + str(z_d)
        print "Measured angles:"
        print "     pitch:" + str(pitch_m)
        print "     roll:" + str(roll_m)
        print "     yaw:" + str(yaw_m)
        # print "     thrust (z):" + str(z_d)    # maybe using some sensor?
        print "PID output angles:"
        print "     pitch:" + str(pitch)
        print "     roll:" + str(roll)
        print "     yaw:" + str(yaw)
        print "     z:" + str(z)

    # using M. Wieremma's thesis
    #u = dyn_model.motor_inversion1(z, roll, pitch, yaw, logging)

    # using Crazyflie's implementation
    #u = dyn_model.motor_inversion2(z, roll, pitch, yaw, logging)

    # using Crazyflie's HACKED implementation
    #u = dyn_model.motor_inversion3(z, roll, pitch, yaw, logging)

    # using M. Wieremma's thesis HACKED impl
    u = dyn_model.motor_inversion4(z, roll, pitch, yaw, logging)

    #if the controller says that the thrust is 0, all motors to 0
    if z_d == 0:
        motorPowerM1 = 0;
        motorPowerM2 = 0;
        motorPowerM3 = 0;
        motorPowerM4 = 0;
    else:
        motorPowerM1 = limitThrust(u[0], limit_thrust);
        motorPowerM2 = limitThrust(u[1], limit_thrust);
        motorPowerM3 = limitThrust(u[2], limit_thrust);
        motorPowerM4 = limitThrust(u[3], limit_thrust);

    if logging:
        #Log the motor powers:
        print "------------------------"
        print "motorPowerM1 (limited):" + str(motorPowerM1)
        print "motorPowerM2 (limited):" + str(motorPowerM2)
        print "motorPowerM3 (limited):" + str(motorPowerM3)
        print "motorPowerM4 (limited):" + str(motorPowerM4)
        print "**************************"

    #Set motor speeds
    motor1.setSpeedBrushless(motorPowerM1)
    motor2.setSpeedBrushless(motorPowerM2)
    motor3.setSpeedBrushless(motorPowerM3)
    motor4.setSpeedBrushless(motorPowerM4)

    #Start Motors
    for mot in motors:
        mot.go()


    #Kalman Prediction
    #MyKalman.predict()


    # calculate the time each iteration takes
    time_u = (dt.datetime.now() - start).microseconds
    time_s = time_u/1e6
    frequency = 1e6/time_u

    # force 50 Hz loop
    if time_s < 20e-3: #50 Hz
        sleep(20e-3 - time_s)

    # # force 60 Hz loop
    # if time_s < 16.66e-3: #50 Hz
    #     sleep(16.66e-3 - time_s)

    # # force 70 Hz loop
    # if time_s < 14.28e-3: #50 Hz
    #     sleep(14.28e-3 - time_s)


    time_u = (dt.datetime.now() - start).microseconds
    frequency = 1e6/time_u
    frequencies.append(frequency)


############################
############################

La salida de este algoritmo se puede ver aqui y aqui. Nada mal para un piloto automático, ¿verdad?

Sin embargo, hay mucho margen de mejora. He aquí algunas sugerencias:

  • Un driver en Python para el MPU9150 que reciba datos en bruto de los giroscopios y magnetómetros y acelerómetros.
  • Implementar un modulo/método/función que devuelva los ángulos de Euler o Quaterniones desde los datos en bruto.
  • Implementar un filtro de Kalman.
  • Mejorar los 50 Hz a 100 Hz o más (para ellos las dos sugerencias anteriores deben de llevarse a cabo).

No dude en contactar con nosotros si está interesado en continuar aprendiendo.

Fuentes:

results matching ""

    No results matching ""