Control of a 28BYJ motor with the ULN2003 circuit

published: 28 October 2020 / updated 28 October 2020

Lire cette page en français

 

Preamble

In the previous article, Controlling a NEMA14 motor with the A4988 driver I had designed the code based on the specifications of the A4988 driver, but without having the hardware at hand.

Here, for this article, I have a step motor and a ULN2003 control circuit available.

In this video by 0033mer, he uses a similar component, a similar ULN2803A integrated circuit at ULN2003:

The FORTH code shown in this video is adapted to the eForth version for ARDUINO.

For us, we will remain faithful to FlashForth by integrating amazing solutions:

Materials needed

List of equipment used:

The 28BYJ-48 5 volt stepper motor

In this photo, five wires come out of the motor: blue, pink, red, orange and yellow.

The red wire brings our 5 Volts (or more for another type of motor).

The other four wires that allow the motor to rotate are: blue, pink, yellow and orange.

Here, the table of the activation sequences of the different wires for a clockwise rotation:

ULN2003 circuit assembly

Here is the connection diagram that inspired me:

In reality, I supply the ARDUINO NANO card from its USB socket. So there is no power supply via the VIN pin.

The stepper motor control program

To use the FORTH code which follows, it is first necessary to compile the contents of the file ARDUINO pins definitions

Physically, the ULN2003 circuit is connected to the ARDUINO board with four wires:

As a reminder, PDx is the short form of P ort D bit x ... which already allows us to declare port D and the connectors that interest us:

decimal
43 constant PORTD
flash
PORTD %00000100 defPIN: IN1 \   blue wire motor
PORTD %00001000 defPIN: IN2 \   pink wire motor
PORTD %00010000 defPIN: IN3 \ yellow wire motor
PORTD %00100000 defPIN: IN4 \ orange wire motor
ram
 
: init.stepper ( ---)
    IN1 output
    IN2 output
    IN3 output
    IN4 output
  ;

Note that the definition of the words IN1 to IN4 is framed by the words flash and ram. If we did not use the word flash, the data would be saved in RAM memory and lost when FlashForth restarts.

In the process, we also define the word init.stepper which declares the four pins PD2 to PD5 as digital outputs.

Using Headerless Definitions

This part of the code deserves detailed explanation.

The word :noname allows you to compile a definition without a visible header in FORTH Dictionary. Once the definition is compiled, it stays on the data stack the cfa address (Code Field Address) of the compiled code.

A FORTH word can be executed as interpreted by simply typing its name. But once compiled, it remains in the definition of a compiled word only the cfa of the compiled words. Example:

: cube ( n --- n^3)
    dup dup * * ;

Pendant la compilation de cube, le compilateur stocke successivement les cfa des mots dup dup * *

Si on dispose de l'adresse cfa d'un mot FORTH, on peut exécuter ce mot par l'intermédiaire du mot execute

Exemple:

variable action
' cube action !      \ push cfa of cube and store it in action
3 action @ execute   \ retrive cfa of the word stored in action and execute it

Si on stocke le cfa d'un autre mot dans action, la séquence action @ execute executera cet autre mot.

We define a pseudo constant 'PULSES which will store the cfa of the following eight headless words:

:noname  \ step 7
    IN4 high  IN3 low   IN2 low   IN1 high ;
:noname  \ step 6
    IN4 low   IN3 low   IN2 low   IN1 high ;
:noname  \ step 5
    IN4 low   IN3 low   IN2 high  IN1 high ;
:noname  \ step 4
    IN4 low   IN3 low   IN2 high  IN1 low  ;
:noname  \ step 3
    IN4 low   IN3 high  IN2 high  IN1 low  ;
:noname  \ step 2
    IN4 low   IN3 high  IN2 low   IN1 low  ;
:noname  \ step 1
    IN4 high  IN3 high  IN2 low   IN1 low  ;
:noname  \ step 0
    IN4 high  IN3 low   IN2 low   IN1 low  ;
flash
create 'PULSES
    , , , , , , , ,
ram

The word 'PULSES is followed by eight words . Word , store the value at the top of the stack in the dictionary and increments the dictionary pointer.

We could have defined words step0 to step7. Example:

: step0  \ step 7
    IN4 high  IN3 low   IN2 low   IN1 low  ;

then define 'PULSES like this:

create 'PULSES
    ' step0 , \ ...etc... ' step7 ,

The word 'PULSES is an array containing eight values, i.e. the cfa of eight definitions without headers. Running 'PULSES drops on the stack the address of the first cfa stored in this array. To access a cfa of rank n in this table, we will therefore execute a sequence of this type n 2 * 'PULSES + @

Here is how we will access the cfa of these definitions without header by creating the word execPulse:

\ pulse delay
2 value pulseDelay
 
\ index pointing in 'PULSES
0 value currentPulse
 
\ send one pulse to A4988
: execPulse ( ---)
    currentPulse 2* \ convert pointer in addr offset
    'PULSES + @     \ extract cfa of noname definition
    execute         \ execute nonamed definition
    pulseDelay ms
  ;

The word execPulse retrieves the content of the value currentPulse which only takes values in the interval 0..7.

The motor will be sent a cycle of 8 steps. This is the task of the following words:

\ send 8 pulses to A4988 in CW direction
: cycleCW ( ---)
    0 to currentPulse
    8 for
        execPulse
        currentPulse 1+ to currentPulse
    next
  ;
 
\ send 8 pulses to A4988 in CCW direction
: cycleCCW ( ---)
    7 to currentPulse
    8 for
        execPulse
        currentPulse 1- to currentPulse
    next
  ;

There are few differences between the words cycleCW and cycleCCW.

In the definition of the word cycleCW we initialize the index currentPulse to zero, then we enter a for..next loop in which we send a step to the motor, then we increment currentPulse.

In the definition of the word cycleCCW we initialize the index currentPulse at seven, we send a step to the motor, then we decrease currentPulse.

It remains to be seen how we determine and choose the direction of rotation of the stepper motor:

defer cycles
 
: setDirection ( n ---)
    0<
    if      ['] cycleCCW is cycles
    else    ['] cycleCW  is cycles
    then
  ;

In the code above, we create the word cycles from defer

The word cycles is a word that has no action until it is indicated not the action to perform.

This is what is done in the word setDirection. If the value n is negative, we run the code ['] cycleCCW is cycles.

From now on, executing cycles will execute cycleCCW

Conversely, if the value is positive, ['] cycleCW is cycles will be executed. Executing cycles will execute cycleCW.

The advantage of using the vectorized execution word cycles allows avoid executing the direction test while sending the steps to the motor.

There remains one final concern to be resolved. Pure formality, but oh so impotent!

If we analyze the timing diagram for triggering steps, we see that there are still not active. Leaving the motor winding under tension will cause the motor to overheat.

To avoid this overheating, all the steps must be deactivated, but by memorizing the last state:

0 value statePORTD
 
: PORTDdown ( ---)
    PORTD c@ to statePORTD
    0 PORTD c!
  ;
 
: PORTDrestore ( ---)
    statePORTD PORTD c!
  ;

The statePORTD value is used to memorize the last state of port D.

The word PORTDdown stores the state of PORT D and resets all exits from this port.

The word PORTDrestore retrieves the last state of port D and reactivates this state.

If we refer to the table titled SWITCHING SEQUENCE, we see that step 1 is a continuation of step 8.

By restoring the last step taken, we start again on a cycle of 8 steps without risking causing movements random engine.

We get to the end by creating the word steps:

: steps ( n ---)
    PORTDrestore
    dup setDirection
    abs
    for
        cycles
    next
    PORTDdown
  ;

The execution of the word steps must be preceded by the number of step cycles to be performed:

\ test stepper motor
init.stepper
200 steps

The full listing is available here: Controlling a 28BYJ motor with the ULN2003 chip