SNVAA20 July   2021 DRV8833 , DRV8833 , LMR33630 , LMR33630

 

  1.   Trademarks
  2. 1Introduction
  3. 2Motorized Resistive Load Architecture
    1. 2.1 Controller Board
    2. 2.2 Resistor Plate
  4. 3Motorized Resistive Load Design
    1. 3.1 Controller Board Design
      1. 3.1.1 Power Management
      2. 3.1.2 Power Converter Selection
      3. 3.1.3 Interface and ADC Selection
    2. 3.2 Resistor Plate Design
      1. 3.2.1 Motor and Motor Driver Selection
      2. 3.2.2 Resistor Track
      3. 3.2.3 Mechanical Arm Assembly
      4. 3.2.4 Feedback Control
  5. 4Thermal Considerations
  6. 5Performance and Results
  7. 6Summary
  8. 7Appendix
    1. 7.1 Controller Board Main Schematic
    2. 7.2 Controller Board Sub-Schematics
    3. 7.3 Resistor Plate Schematics
    4. 7.4 Python Code

Python Code

# Motorized Resistive Load - Python Code by Abdallah Obidat

# Note that the Raspberry Pi was configured to boot into this program. This can be achieved in a number of ways.

import smbus as i2cBUS  # import smbus to allow control of the I2C bus
import RPi.GPIO as GPIO  # import RPi.GPIO to allow control of the GPIO pins
import time as tm  # import time to allow the insertion of time delays in the code

bus = i2cBUS.SMBus()  # create a SMBus object
bus.open(1)  # open communication line of the i2c bus object - port 1 is being used here

display_register_address = 0x3C
tm.sleep(5)  # wait for two seconds after initializing the i2c bus

# the last value turns the display on or off (0x08 = OFF, 0x0F = ON)
initialization_list = [0x3A, 0x09, 0x06, 0x1E, 0x39, 0x1B, 0x6E, 0x56, 0x7A, 0x38, 0x0F]

# add 1 to the display reg address to set the write bit high
bus.write_i2c_block_data(display_register_address+1, 0x00, initialization_list)

# Create a dictionary to easily output words on the display
Alpha = {"A": 0x41, "B": 0x42, "C": 0x43, "D": 0x44, "E": 0x45, "F": 0x46, "G": 0x47, "H":0x48, "I": 0x49, "J": 0x4A,
         "K": 0x4B, "L": 0x4C, "M": 0x4D, "N": 0x4E, "O": 0x4F, "P": 0x50, "Q": 0x51, "R": 0x52, "S":0x53, "T": 0x54,
         "U": 0x55, "V": 0x56, "W": 0x57, "X": 0x58, "Y": 0x59, "Z": 0x5A, "Ohm": 0xB5, ":": 0x3A, "-": 0x2D}

Num = {"0": 0x30, "1": 0x31, "2": 0x32, "3": 0x33, "4": 0x34, "5": 0x35, "6": 0x36, "7": 0x37, "8": 0x38, "9": 0x39,
       ".": 0x2E}

# select ROM_A for the display
# reg values: RE high, ROM selection, ROM choice (0x00 = ROM_A, 0x04 = ROM_B, 0x08 = ROM_C)
# note that 0x40 or 0x5F need to be written in order to actually select the ROM, bits are 10XX XXXX
bus.write_i2c_block_data(display_register_address+1, 0x00, [0x3A])
bus.write_i2c_block_data(display_register_address+1, 0x00, [0x72])
bus.write_i2c_block_data(display_register_address+1, 0x40, [0x00])
bus.write_i2c_block_data(display_register_address+1, 0x00, [0x38])

# set display to double height mode
bus.write_i2c_block_data(display_register_address+1,0x00,[0x3A, 0x1B, 0x3C])

# clear display
bus.write_i2c_block_data(display_register_address+1, 0x00, [0x01])

direction = "clockwise"  # set direction global variable

# The motor used has 200 steps per full revolution, which is 1.8 degrees per step
measured_step = 0  # variable used to show position on LCD display

# set ADC variables
VDD = 3.3  # VDD and full scale voltage Of ADC
ADC_Bits = 12  # the ADC121C021 is a 12-bit ADC
ADC_full_scale = (2**ADC_Bits)-1  # 2^12 States for the ADC output, but indexing starts at 0, so the variable is
# decremented by 1

# ADC1 is used for the set value and reads in the voltage controlled by the potentiometer knob
ADC_1_device_address = 0x50 # the device address of the ADC121C021, which is assigned as a hexadecimal number
ADC_1_result_register_address = 0x0
final_read_result_1 = ""  # initialize a blank string that will form the final read result

# ADC2 is used for positional feedback and reads the voltage on the dynamic resistive divider to provide current
# location information
ADC_2_device_address = 0x51
ADC_2_result_register_address = 0x0
final_read_result_2 = ""

# create a list of 0 ohm values that corresponds to the length of the track where a short exists
R_set0 = [0 for i in range(1, 7)]

# there are 30x 0.050 Ohm resistors spaced ~two steps from each other
# this list comprehension creates a list of sequenced resistances that matches the actual design of the motorized load
# and is interleaved with itself to double its size
R_set1 = [round(i * 0.05, 2) for i in range(0, 31)]
R_set1 = [value for pair in zip(R_set1, R_set1) for value in pair]

# there are 30x 0.10 Ohm resistors spaced ~two steps from each other
# this list comprehension creates a list of sequenced resistances that matches the actual design of the motorized load
# and is interleaved with itself to double its size
R_set2 = [round((i + 1) * 0.10 + R_set1[-1], 2) for i in range(0, 30)]
R_set2 = [value for pair in zip(R_set2, R_set2) for value in pair]

# there are 24x 1.5 Ohm resistors spaced ~three steps from each other
# this list comprehension creates a list of sequenced resistances that matches the
# motorized load and is interleaved with itself to double its size
R_set3 = [round((i + 1) * 1.5 + R_set2[-1], 2) for i in range(0, 24)]
R_set3 = [value for pair in zip(R_set3, R_set3, R_set3) for value in pair]

# merge the interleaved lists together to create a "set" array of resistors,
# the values of which correspond to the steps that the motor can target
R_set = R_set0 + R_set1 + R_set2 + R_set3

# create a list of feedback voltages that correspond to different loads
V_fb = list()

# initialize a list to store the current state of the motor's logic inputs
state_list = [GPIO.HIGH, GPIO.HIGH, GPIO.LOW, GPIO.LOW]


def RotateOneStepClockwise(state_list_param):
    # define an interrupt/callback that accepts the global variable "state_list" to single rotate the motor,
    # by a single clockwise step, based on the current state of the motor driver logic inputs
    
    global Delay  # allow the callback to access the global "Delay" variable
    global VDD
    global ADC_Bits
    global ADC_full_scale
    global ADC_1_device_address
    global ADC_1_result_register_address
    global final_read_result_1
    global ADC_2_device_address
    global ADC_2_result_register_address
    global final_read_result_2
    
    # initialize the new state list variable "new_state_list"
    new_state_list = [None, None, None, None]
    
    # set the new state, based on the current state, such that the motor rotates by a single clockwise step
    new_state_list[0] = state_list[3]
    new_state_list[1] = state_list[0]
    new_state_list[2] = state_list[1]
    new_state_list[3] = state_list[2]

    # set the Raspberry Pi digital outputs (motor drive logic inputs) according to the newly determined state,
    # which is based on the current state
    GPIO.output(6, state_list[0])
    GPIO.output(19, state_list[1])
    GPIO.output(13, state_list[2])
    GPIO.output(26, state_list[3])
    
    tm.sleep(Delay)  # delay between steps according to the global "Delay" variable

    # read in two bytes (16 bits) of data
    ADC_2_read_result = bus.read_i2c_block_data(ADC_2_device_address, ADC_2_result_register_address, 2)
    
    # concatenate results to form final read result
    # the first byte returned is shifted by 8 digits before concatenating results
    final_read_result_2 = ((ADC_2_read_result[0]) << 8) | (ADC_2_read_result[1])

    # for debug
    # final_read_voltage/VDD = final_read_result/ADC_full_scale -> solve for final_read_voltage
    # current_read_voltage = VDD*(final_read_result)/ADC_full_scale -> convert the reading into a voltage
    
    return new_state_list  # return the new state of the motor drive inputs

def RotateOneStepCounterClockwise(state_list_param):
    # define an interrupt/callback that accepts the global variable "state_list" to single rotate the motor,
    # by a single counterclockwise step, based on the current state of the motor driver logic inputs
    
    global Delay  # allow the callback to access the global "Delay" variable
    global VDD
    global ADC_Bits
    global ADC_full_scale
    global ADC_1_device_address
    global ADC_1_result_register_address
    global final_read_result_1
    global ADC_2_device_address
    global ADC_2_result_register_address
    global final_read_result_2
    
    # initialize the new state list variable "new_state_list"
    new_state_list = [None, None, None, None]
    
    # set the new state, based on the current state, such that the motor rotates by a single step
    new_state_list[0] = state_list[1]
    new_state_list[1] = state_list[2]
    new_state_list[2] = state_list[3]
    new_state_list[3] = state_list[0]

    # set the Raspberry Pi digital outputs (motor drive logic inputs) according to the newly determined state,
    # which is based on the current state
    GPIO.output(6, state_list[0])
    GPIO.output(19, state_list[1])
    GPIO.output(13, state_list[2])
    GPIO.output(26, state_list[3])
    
    tm.sleep(Delay)  # delay between steps according to the global "Delay" variable
    
    # read in two bytes (16 bits) of data
    ADC_2_read_result = bus.read_i2c_block_data(ADC_2_device_address,ADC_2_result_register_address,2)

    # concatenate results to form final read result
    # the first byte returned is shifted by 8 digits before concatenating results
    final_read_result_2 = ((ADC_2_read_result[0]) << 8) | (ADC_2_read_result[1])

    # for debug
    # final_read_voltage/VDD = final_read_result/ADC_full_scale -> solve for final_read_voltage
    # current_read_voltage = VDD*(final_read_result)/ADC_full_scale -> convert the reading into a voltage
 
    return new_state_list  # return the new state of the motor drive inputs

def MotorStepInterrupt(self):
    # define an interrupt/callback that accesses a number of global variables and controls the motor movement to move it
    # from its current position to the desired position, such that the target resistance is formed between its terminals
    # 
    # The callback/interrupt wakes up the motor drive chip. It then determines the direction of rotation and enters a
    # while loop. After entering the loop, it will call either the "RotateOneStepClockwise" function or the
    # "RotateOneStepCounterClockwise" function depending on the "direction" variable value. The loop exits once the
    # final voltage target obtained via the first ADC matches the voltage, within an acceptable tolerance, on the second
    # ADC. The current state of the motor drive logic inputs are tracked and updated with each iteration in the while
    # loop.
    
    # declare global variables that the callback/interrupt will need access to
    global direction
    global measured_step
    global state_list
    global VDD
    global ADC_Bits
    global ADC_full_scale
    global ADC_1_device_address
    global ADC_1_result_register_address
    global final_read_result_1
    global ADC_2_device_address
    global ADC_2_result_register_address
    global final_read_result_2
    global R_set
      
    # read in two bytes (16 bits) of data
    ADC_1_read_result = bus.read_i2c_block_data(ADC_1_device_address, ADC_1_result_register_address, 2) 
    
    # concatenate results to form final read result
    # the first byte returned is shifted by 8 digits before concatenating results
    final_read_result_1 = ((ADC_1_read_result[0]) << 8) | (ADC_1_read_result[1])
    
    # convert the ADC reading into a target voltage
    final_read_voltage = VDD*final_read_result_1/ADC_full_scale 
    print('ADC Reading = '+str(final_read_voltage) + ' V')

    # read in two bytes (16 bits) of data
    ADC_2_read_result = bus.read_i2c_block_data(ADC_2_device_address, ADC_2_result_register_address, 2) 
    
    # concatenate results to form final read result
    # the first byte returned is shifted by 8 digits before concatenating results
    final_read_result_2 = ((ADC_2_read_result[0]) << 8) | (ADC_2_read_result[1])

    # convert the ADC reading into a voltage that represents current position
    current_read_voltage = VDD*final_read_result_2/ADC_full_scale 

    # select the target resistance based on the knob (potentiometer) voltage reading
    R_target_index = round((final_read_result_1/ADC_full_scale)*len(R_set))
    
    # do not allow the motor to turn more than a full rotation to get to its target
    if R_target_index > 199:
        R_target_index = 199
    
    # select the target resistance which is equal to the desired load resistance
    R_target = R_set[R_target_index]
    
    # choose a threshold that defines the range for an acceptable reading - this changes based on resistor values used
    # these values were verified empirically for this specific design
    if R_target < 1.5:
        tolerance = 0.001
    elif R_target > 4.5:
        tolerance = 0.030
    else:
        tolerance = 0.001
    
    # calculate Vdiv, the expected voltage at the position of interest
    final_voltage_target = ((3.3/(49.9+40.5-R_target))*(40.5-R_target))
    
    # determine direction of rotation based on the current position and target resistance
    if final_voltage_target > current_read_voltage:
        direction = "counter_clockwise"
    else:
        direction = "clockwise"

    i = 0  # reset index
    while 1:
        Delay = 10/1000  # sets an additional delay between rotations
        current_read_voltage = VDD*final_read_result_2/ADC_full_scale
        if direction == "clockwise":
            state_list = RotateOneStepClockwise(state_list)  # clockwise rotation
        else:
            state_list = RotateOneStepCounterClockwise(state_list)  # counterclockwise rotation
        i = i+1  # increment the counter after either function is called

        if (abs(final_voltage_target - current_read_voltage) < tolerance) or (i > 199):  # while loop exit condition
            measured_step = R_target_index
            print("R_target: " + str(R_target))
            print("final_voltage_target: " + str(final_voltage_target))
            print("current_read_voltage: " + str(current_read_voltage))
            break

    # additional delay
    tm.sleep(Delay)


# main code

GPIO.setwarnings(False)  # disable warnings that occur when configuring the GPIO signals

# refer to the GPIO signals based on the broadcom chip number and not the board's breakout header containing the pins
GPIO.setmode(GPIO.BCM)

GPIO.setup(1, GPIO.IN, GPIO.PUD_UP) # set GPIO 1 as a digital input pin and have it pulled up by default

# configure a trigger event for the callback/interrupt "MotorStepInterrupt". The trigger is set to occur when a digital
# falling edge occurs on GPIO 5 and a debounce time of 3000ms is set following an event
GPIO.add_event_detect(1, GPIO.FALLING, callback=MotorStepInterrupt, bouncetime=3000)
 
# enable/wakeup the motor drive IC
GPIO.setup(0, GPIO.OUT)
GPIO.output(0, GPIO.HIGH)

# set all of the GPIO signals that will be designated as logic inputs to the motor drive to be digital outputs on the
# Raspberry Pi
GPIO.setup(6, GPIO.OUT)
GPIO.setup(19, GPIO.OUT)
GPIO.setup(13, GPIO.OUT)
GPIO.setup(26, GPIO.OUT)

# set all of the digital inputs to the motor drive to logic low initially
GPIO.output(6, GPIO.LOW)
GPIO.output(19, GPIO.LOW)
GPIO.output(13, GPIO.LOW)
GPIO.output(26, GPIO.LOW)

Delay = 10  # Delay in s
Delay = Delay/1000  # Delay in ms

while 1:
    
    # setting all of the  the Raspberry Pi outputs (motor drive logic inputs) has almost the same effect
    # as disabling the motor drive IC. This reduces the consumed power while the motor isn't in motion,
    # but doesn't protect the motor from unwanted rotation due to any potential external torque sources
    GPIO.output(6, GPIO.LOW)
    GPIO.output(19, GPIO.LOW)
    GPIO.output(13, GPIO.LOW)
    GPIO.output(26, GPIO.LOW)
    
    # read in two bytes (16 bits) of data
    ADC_1_read_result = bus.read_i2c_block_data(ADC_1_device_address, ADC_1_result_register_address, 2)

    # concatenate results to form final read result
    # the first byte returned is shifted by 8 digits before concatenating results
    final_read_result_1 = ((ADC_1_read_result[0]) << 8) | (ADC_1_read_result[1])
    final_read_voltage = VDD*final_read_result_1/ADC_full_scale

    # user's current resistor selection to be displayed on LCD display
    current_step = round(final_read_result_1*199/ADC_full_scale)

    # prepare strings to be displayed on LCD display
    set_string = "MEAS:"+str(R_set[round(measured_step)])
    meas_string = "SET:"+str(R_set[round(current_step)])
    set_string_list = list()
    meas_string_list = list()

    for character in set_string:
        try:
            set_string_list.append(Alpha[character])
        except:
            set_string_list.append(Num[character])
        
    for character in meas_string:
        try:
            meas_string_list.append(Alpha[character])
        except:
            meas_string_list.append(Num[character])
            
    set_string_list.append(Alpha["Ohm"])
    meas_string_list.append(Alpha["Ohm"])
    
    # clear display 
    bus.write_i2c_block_data(display_register_address+1, 0x00, [0x01])

    # display resistor value to be set (SET) on LCD display
    bus.write_i2c_block_data(display_register_address+1, 0x40, set_string_list)
    
    # move cursor position to 1st position on 1st row
    bus.write_i2c_block_data(display_register_address+1, 0x00, [0xA0])

    # display current resistance (MEAS) on LCD display
    bus.write_i2c_block_data(display_register_address+1, 0x40, meas_string_list)
        
    # add a delay of at least half a second between setting the GPIO low and then setting it high.
    tm.sleep(500*1/1000)

# close I2C communication bug - code only reachable while debugging
# bus.close() # close communication line of the bus object