RTC Website

Finite state machine and open-loop control

TX

The objectives of this exercise are:

  1. To implement a finite state machine control algorithm
  2. To understand pulse-modulation driving of a DC motor
  3. To use instruction timing to produce a calibrated delay
  4. 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.

A schematic of the pulse modulation via \mathit{run} DIO output (DIO3), the speed measurement via the FPGA encoder input (DIO0, DIO4), and the switches \mathit{printS} and \mathit{stopS}.

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_Status EncoderC_initialize(NiFpga_Session myrio_session,
                                  MyRio_Encoder *channel);

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:

MyRio_Encoder *encC0;
EncoderC_initialize(myrio_session, encC0);

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;
EncoderC_initialize(myrio_session, &encC0);

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.

 Figure 4.19
Figure 4.19: The output PWM signal run and the corresponding FSM states for when the print switch is unpressed (printS = 0) and when it is pressed (printS = 1).

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:

  1. Use MyRio_Open() to open the myRIO session, as usual.
  2. Set up all interface conditions and initialize the FSM using initializeSM(), described next.
  3. Request, from the user, the number (\(N\)) of wait intervals in each BTI. (use double_in()).
  4. Request the number (\(M\)) of intervals that the motor signal is “on” in each BTI.
  5. Start the main state transition loop.
  6. When the main state transition loop detects that the current state is \(\mathit{exit}\), it should close the myRIO session, as usual.

State transition diagram.

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:

  1. 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}\),

    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;
  2. Initialize the encoder interface, as described previouosly.

  3. Stop the motor (set \(\mathit{run}\) to \(0\)).

  4. Set the initial state to \(\mathit{low}\).

  5. Set the clock to 0.

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 function vel reads the encoder counter and computes the speed in units BDI/BTI. See vel. 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:

  1. Read the current encoder count: \(c_n\) (interpreted as an 32-bit signed binary number, int).
  2. Compute the speed as the difference between the current and previous counts: \((c_n - c_{n-1})\).
  3. Replace the previous count with the current count for use in the next BTI.
  4. 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.

stop

The final state of the program performs the following operations:

  1. Stop the motor. That is, set \(\mathit{run}\) to \(0\).
  2. Clear the LCD and print the message: stopping.
  3. Set the current state to \(\mathit{exit}\). The while loop in main() should terminate if the current state is \(\mathit{exit}\).
  4. 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:, the LDR instruction executes in two clock cycles (the “fastforward” case applies). The clock frequency of the processor is 667 MHz, so the time for one LDR instruction is \[ 2\times\frac{1}{667}\mu\text{s} \] However, that LDR instruction is within the while loop, which is implemented in the assembly code by the branch instruction BNE back to LOOP:. So the instructions within the while loop are executed 417,000 times. Clearly, instructions within that loop will dominate the total real-time delay of the wait() 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 the wait() 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

  1. The emulator runs only at a 100 percent duty cycle.
  2. You will not be able to view your PWM waveform.
  3. Because there is no stop switch when using the emulator, declare an integer, i, initially set to 125 in the high() state function. Decrement i each time low() is executed. When i is zero, go to the \(\mathit{stop}\) state.
  4. 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 .

 Figure
Figure : Figure generated in Matlab.

Include a photo figure, as follows, see .

 Figure
Figure : A photo figure example.

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:

  1. What is wrong?
  2. How would modifying the state transition diagram correct this problem?
  3. How would you modify the state transition table?
  4. 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.

MyRio_Encoder *encC0;
EncoderC_initialize(myrio_session, encC0);

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.


  1. See section A.2 for details of other specific target systems.↩︎

Online Resources for Section L4

No online resources.