Finite state machine and open-loop control
The objectives of this exercise are:
- To implement a finite state machine control algorithm
- To understand pulse-modulation driving of a DC motor
- To use instruction timing to produce a calibrated delay
- To become familiar with quadrature encoders
The T1 target system will be required, but only the components listed in appendix B for this lab exercise. Set up each component in accordance with the instructions for your specific T1 version in section A.2. Connect your components in the manner described in the “Online Resources” section of the web page for this lab exercise: https://rtcbook.org/1h.
Introduction
In this exercise, we will write a program to control and monitor the speed of a DC motor implemented using a finite state machine (FSM) . The myRIO target computer will drive the motor with pulse modulation on a DIO channel configured as a digital output. This digital signal will be amplified by the analog current amplifier, as shown in . The speed of the motor will be inferred from a quadrature encoder on the motor and read by the myRIO field-programmable gate array (FPGA) encoder counter. During operation, two mechanical switches connected to myRIO DIO inputs will control printing the motor speed on the LCD and stopping the system.
Pulse-width modulation
As shown in , DIO3
of
connector C produces a digital signal called \(\mathit{run}\). That signal is connected to
a current source amplifier that drives the motor. When \(\mathit{run}=0\), no current flows through
the motor; and when \(\mathit{run}\) is
1, 0.05 amps flow.1 Your program will periodically alter
this digital signal, applying an oscillating signal to the motor. The
duty cycle (the percentage of time power is
applied) is the percentage of time that the DIO channel is low.
Encoder/Counter
A quadrature encoder is mounted on the
shaft of the DC motor. The encoder has \(C=512\) counts per revolution (CPR), which
corresponds to \(4 C=2048\) state
changes per revolution. Therefore, each encoder state change corresponds
to a motor rotation of \(1/(4
C)=1/2048\) a revolution, called a basic
displacement increment (BDI).
An encoder counter in the FPGA interface determines the total number of these state changes. The speed is determined by computing the number of state changes from the encoder during a specific time interval, called the basic time interval (BTI). Therefore, the number of state changes occurring during each interval represents the angular speed of rotation in units of BDI/BTI.
Initializing the Encoder Counter
Counting of the encoder state changes is accomplished by the FPGA associated with the Xilinx Zynq-7010 SoC, with dual Cortex A-9 ARM processors. The counter must be initialized before it can be used. Initialization includes identifying the encoder connection, setting the count value to zero, configuring the counter for a quadrature encoder, and clearing any error conditions. A function, included in the T1 C library, alters the appropriate control registers to initialize of the encoder interface on connector C.
The prototype for the initialization function is
(NiFpga_Session myrio_session,
NiFpga_Status EncoderC_initialize*channel); MyRio_Encoder
The first argument, myrio_session
(type: NiFpga_Session
), identifies the
FPGA session, and must be declared as a global variable for
this application. That is,
; NiFpga_Session myrio_session
The second argument channel
(type:
MyRio_Encoder *
)
points to a structure that maintains the current status and count value,
and must also be declared as a global variable. We will use the ENC0
channel.
; MyRio_Encoder encC0
A Common Error
When supplying the second argument to EncoderC_initialize()
,
you might be tempted to declare a pointer to a MyRio_Encoder
structure and pass that
pointer to the function as shown here:
*encC0;
MyRio_Encoder (myrio_session, encC0); EncoderC_initialize
The complier will not issue an error. However, the code it will not
work. Although, the declaration initializes a pointer that can point to
a MyRio_Encoder
structure, it does not
allocate memory for that structure. Therefore, when code attempts to
read or write to this memory location, it will access invalid or
nonexistent memory.
Instead, the MyRio_Encoder
structure should be declared. The address operator can then be used to
pass a pointer to the MyRio_Encoder
structure to the EncoderC_initialize()
function as shown here
;
MyRio_Encoder encC0(myrio_session, &encC0); EncoderC_initialize
This code both allocates memory for the MyRio_Encoder
structure, and passes its
address to EncoderC_initialize()
.
Reading the Encoder Counter
The position of the encoder (in BDI) may be found at any time by reading the counter value. The prototype of a library function provided for that purpose is
uint32_t Encoder_Counter(MyRio_Encoder *channel);
where the argument is the counter channel declared during the initialization, and the returned value is the current count in the form of a 32-bit integer.
Writing the program
The
main
function
Write a main()
function that
continuously produces a periodic waveform on \(\mathit{run}\), which will apply an average
current to the motor determined by the duty cycle. The waveform period,
(1 BTI) will be \(\mathit{N}\) “wait”
intervals. Current will pass through the motor during the first \(\mathit{M}\) wait intervals (\(0 < M < N\)). See the first graph in
figure 4.19.
In addition, while DIO1
of connector
C (\(\mathit{printS}\)) is \(1\), the program will print the measured
speed on the display at the beginning of each BTI. The user will control
DIO1
through a push-button switch. The
corresponding \(\mathit{run}\) waveform
is shown in the second graph of figure 4.19.
The algorithm should be implemented as an FSM (see section 4.5). As shown in the state transition diagram in
, the machine will have five possible states: \(\mathit{high}\), \(\mathit{low}\), \(\mathit{speed}\), \(\mathit{stop}\), and (the terminal) \(\mathit{exit}\). The inputs will be the
\(\mathit{clock}\) variable, and \(\mathit{printS}\) (DIO1
) and \(\mathit{stopS}\) (DIO5
) mechanical switches. The outputs will
be
- \(\mathit{run}\): the PWM signal
- \(\mathit{clock}\): the “clock” counter (which sometimes needs reset to \(0\))
- \(\mathit{display}\): printing to the LCD, e.g., motor speed: \(\mathit{display} \gets \text{speed}\)
- \(\mathit{buffer}\): the program array that stores motor speed measurements
- \(\mathit{file}\): the file to which \(\mathit{buffer}\) should be written
The corresponding state transition table, listing all possible transitions, is shown in table 4.5.
Overall, the main()
program will
do the following:
- Use
MyRio_Open()
to open the myRIO session, as usual. - Set up all interface conditions and initialize the FSM using
initializeSM()
, described next. - Request, from the user, the number (\(N\)) of wait intervals in each BTI. (use
double_in()
). - Request the number (\(M\)) of intervals that the motor signal is “on” in each BTI.
- Start the main state transition loop.
- When the main state transition loop detects that the current state is \(\mathit{exit}\), it should close the myRIO session, as usual.
Other functions
In addition to main()
, several
functions will be required, as described next. These functions include
one for each state: high()
, low()
, speed()
, and stop()
.
double_in
-
To execute the user I/O, you may use the routine
double_in()
developed in lab 1, or you may simply call it from the T1 C library:double double_in(char *string);
initializeSM
-
Perform the following:
Initialize DIO channels 1, 3, and 5 on connector C, in accordance with , by specifying to which register each DIO corresponds. Naming the channels after the corresponding signal names (\(\mathit{printS}\), and \(\mathit{run}\), \(\mathit{stopS}\)) will make the code more readable. For example, for
DIO3
, with signal name: \(\mathit{run}\),.dir = DIOC_70DIR; // "70" used for DIO 0-7 run.out = DIOC_70OUT; // "70" used for DIO 0-7 run.in = DIOC_70IN; // "70" used for DIO 0-7 run.bit = 3; run
Initialize the encoder interface, as described previouosly.
Stop the motor (set \(\mathit{run}\) to \(0\)).
Set the initial state to \(\mathit{low}\).
Set the
clock
to0
.
low
-
If
clock
is \(N\), set it to \(0\), set \(\mathit{run}\) to \(1\), and if \(\mathit{printS}\) is \(1\), change the state to \(\mathit{speed}\). If \(\mathit{stopS}\) is \(1\), change the state to \(\mathit{stop}\). Otherwise, change the state to \(\mathit{high}\). high
-
If
clock
is M, set \(\mathit{run}\) to \(0\), and change the state to \(\mathit{low}\). speed
-
Call
vel
. The functionvel
reads the encoder counter and computes the speed in units BDI/BTI. Seevel
. Convert the speed to units of RPM. Print the speed as follows:printf_lcd("\fspeed: %g rpm",rpm);
Finally, change the state to \(\mathit{high}\). vel
-
Write a function to measure the velocity. Each time this subroutine is called, it should perform the following functions. Suppose that this is the start of the \(n\)th BTI:
- Read the current encoder count: \(c_n\) (interpreted as an 32-bit signed
binary number,
int
). - Compute the speed as the difference between the current and previous counts: \((c_n - c_{n-1})\).
- Replace the previous count with the current count for use in the next BTI.
- Return the speed
double
to the calling function.
The first time
vel
is called, it should set the value of the “previous” count to the current count. - Read the current encoder count: \(c_n\) (interpreted as an 32-bit signed
binary number,
stop
-
The final state of the program performs the following operations:
- Stop the motor. That is, set \(\mathit{run}\) to \(0\).
- Clear the LCD and print the message:
stopping
. - Set the current state to \(\mathit{exit}\). The
while
loop inmain()
should terminate if the current state is \(\mathit{exit}\). - Save the response to a MATLAB file.
wait
-
Your program will determine the time by executing a calibrated delay-interval function. Consider the
wait
function in lst. ¿lst:lab-4-wait?.void wait(void) { uint32_t i; i = 417000; while (i > 0) { i--; } return; }
Notice that this function does nothing but waste time! The compiler generates for this function the assembly language codes shown in lst. ¿lst:wait-disassembly?, which have been edited to utilize labels for readability.
WAIT: PUSH {R11} ADD R11, SP, #0 SUB SP, SP, #12 MOV R3, #417000 STR R3, [R11, #-8] B START LOOP: LDR R3, [R11, #-8] SUB R3, R3, #1 STR R3, [R11, #-8] START: LDR R3, [R11, #-8] CMP R3, #0 BNE LOOP NOP ADD SP, R11, #0 LDMFD SP!, {R11} BX LR
The number of processor clock cycles required for each instruction to execute is given in ARM2014b. For instance, at label
START:
, theLDR
instruction executes in two clock cycles (the “fastforward” case applies). The clock frequency of the processor is 667 MHz, so the time for oneLDR
instruction is \[ 2\times\frac{1}{667}\mu\text{s} \] However, thatLDR
instruction is within thewhile
loop, which is implemented in the assembly code by the branch instructionBNE
back toLOOP:
. So the instructions within thewhile
loop are executed 417,000 times. Clearly, instructions within that loop will dominate the total real-time delay of thewait()
function.Note carefully how the branch instructions are used. shows the clock cycles for the instructions used in the
wait()
function. In problem L4.1, you will compute the number of clock cycles in thewait()
function.Table 1: Opcode times (CPU cycles), for instructions used in wait()
Instruction Type Mnemonic Clock Cycles Load/store registers LDR
,STR
2 Branch BNE
,B
,BX
0 Stack PUSH
,LDMFD
2 Arithmetic ADD
,SUB
,CMP
1 Move MOV
1 No operation NOP
1
Header files
The following header files will be required by your code:
#include <stdio.h>
#include "Encoder.h"
#include "MyRio.h"
#include "DIO.h"
#include "T1.h"
#include "matlabfiles.h"
Emulation: a Debugging Aid
To fully debug and test your code, your development system laptop must be connected to a myRIO with the amplifier, motor, and encoder attached as described in section A.2. You will be able to observe the motor motion and see your pulse modulation waveform on an oscilloscope.
However, as a debugging aid, our T1 C library includes a software emulation (a dynamic model) of the PWM digital channel, the encoder, the amplifier, and the motor. The emulator allows you to run your code and save the MATLAB file without needing to be connected to the amplifier and motor. By this means, you can debug your program on a myRIO without the hardware.
To activate the emulator, #include
the header
file emulate.h
. Without
any other changes in your code, the I/O for the DIO and Encoder
interfaces is redirected through the emulator. When you want to execute
your code on a myRIO using the hardware interfaces, comment out the
included emulator header and rebuild the project.
Emulator Limitations
- The emulator runs only at a 100 percent duty cycle.
- You will not be able to view your PWM waveform.
- Because there is no stop switch when using the emulator, declare an
integer,
i
, initially set to 125 in thehigh()
state function. Decrementi
each timelow()
is executed. Wheni
is zero, go to the \(\mathit{stop}\) state. - Because there is no print switch when using the emulator, always print to the LCD screen by temporarily forcing the \(\mathit{low}\) state to always transfer to the \(\mathit{speed}\) state.
Debugging with Expressions
Recall that the current state and the clock are stored in global variables. These variables can be viewed using expressions, which are snippets of C code that are evaluated whenever execution is paused. You can view the expression information in the Expressions view of the debugger.
Laboratory exploration and evaluation
Determine the exact number of clock cycles for the wait()
function of
lst. ¿lst:lab-4-wait? to execute, accounting for all
instructions (use ). From that, calculate the delay
interval in milliseconds. A spreadsheet is convenient for making this
calculation.
Examine the circuit board attached to connector C of the myRIO. The normally open push-button switches, when closed, connect DIO channels 1 and 5 to \(V_\text{CC} = 5\) V when pressed, as shown in . Note: These channels have pull-down resistors. Use the oscilloscope to view the waveform produced by your program. For example, use \(N = 5\), \(M = 3\).
TODO explain
Reference the listing: lst. ¿lst:lab-4-main?
Reference a line number: lab1:nifpga_status
Include a figure, as follows, see .
Include a photo figure, as follows, see .
Reference another lab: lab 3.
Compare your program’s printed output (on the LCD) to the steady state motor speed as measured with an optical tachometer.
Use the oscilloscope to view the PWM waveform produced by your program, and to measure the actual length of a BTI. Is it what you expect? If not, why not?
Repeat problem L4.4 while printing the speed (pressing the switch). What does the oscilloscope show has happened to the length of the BTI? What’s going on!?
Describe how you made the measurements of problem L4.4, problem L4.5 and discuss any limitations in the accuracy of the timing of the output. In lab 6, we will find ways of overcoming this limitation.
After you have your code running as described above, try this: Record the velocity step response of the DC motor, save it to a file, and plot it in MATLAB. Here’s how:
Add code to your speed()
function to
save the measured speed at successive locations in a global buffer. You
will need to keep track of a buffer pointer in a separate memory
location. Increment the buffer pointer each time a value is put in the
buffer. The program must stop putting values in the buffer when it is
full. For example,
#define IMAX 200 // max points
static double buffer[IMAX]; // speed buffer
static double *bp = buffer; // buffer pointer
In the speed()
state
function, include
if (bp < buffer + IMAX) *bp++ = rpm;
To record an accurate velocity, temporarily comment out the printf_lcd()
statement in speed()
and hold
down the \(\mathit{printS}\) switch
while you run the program.
The program should save the response stored in the buffer to a MATLAB (.mat) file on the myRIO under the Real-Time Linux operating system’s file system during the \(\mathit{stop}\) state. See section D.1 for more details.
Name the data file Lab4.mat. In the file,
save the speed buffer, the values of N
and M
, and a character string
containing your name. The name string will allow you to verify that the
file was filled by your program.
You may have noticed that when \(M = 1\), the FSM does not function as desired. Address the following issues:
- What is wrong?
- How would modifying the state transition diagram correct this problem?
- How would you modify the state transition table?
- Modify your program to correct the \(M = 1\) case. Test the result.
A programmer has written following code in an attempt to initialize the encoder.
*encC0;
MyRio_Encoder (myrio_session, encC0); EncoderC_initialize
It’s incorrect. What issues could this code cause, and why?
Some Afterthoughts
In this lab, we have taken the first steps to realize precise timing for a real-time program. Our goal has been to produce a waveform of constant period. Observing the waveform on an oscilloscope demonstrated that accurate “wait” intervals can be programmed using instruction timing. However, during printing, the waveform period was inaccurate due to the comparatively long and unpredictable print event. So, the general use of “wait” intervals as means of timing is ultimately limited. We require a timing method that is independent of other calculations. In the following chapters, we will implement an autonomous means of timing using “interrupts.”
This lab also used the generation of a pulse modulation signal to illustrate the application of a finite state machine. You should be aware that many microcontrollers include dedicated digital circuitry for generating PWM signals. Typically, the frequency and duty cycle of the PWM interface are program controlled. For example, the myRIO FPGA can independently produce PWM signals at frequencies as high as \(10\) MHz. A sample PWM project is available in the myRIO C library.
See section A.2 for details of other specific target systems.↩︎
Online Resources for Section L4
No online resources.