You are on page 1of 30

LAB 4

DC motor speed control using pulse


width modulation (PWM)

Submitted by
Vishal Bharam and Bicky Shakya
ENGR 323
Prof. Taikang Ning









Introduction
As we saw in the previous projects, 8051 microcontroller embedded into other systems can perform
multiple complicated operations at the same time. 8051 microcontrollers are used in a wide spectrum of
applications, such as in the field of automobiles, video games, microwaves and many others. In this
laboratory, we use the 8051 microcontroller to design an embedded system that would run according to
allowable target speed desired by the user.
Problem Statement: This design laboratory is to help us learn and become familiar with designing and
implementing a DC motor speed control embedded system, which involves Pulse-Width Modulation
(PWM) and the proportional, integral and derivative (PID) controller design.
Specific Design Goal: The design goal of this lab is to use the SilliconLab-C8051F120 kit to implement a
PID controller that allows the user to enter the desired target speed. With feedback, the PID controller can
make timely adjustment to the PWM to accommodate the changing shaft-load to maintain the desired
rotating speed. The system block diagram for the system is given in fig.1.

Fig 1: System configuration of DC motor speed control

Following constrains are to be taken into the consideration while designing the lab:
Allow users to easily adjust the desired motor rpm speed
Use the PWM signal to control the DC motor speed
Use the pushbutton to control the rotating direction
Implement a properly working adjustable feedback controller
Display the system information (desired speed and actual speed of the DC motor ) using a
16x2 LCD

Hardware:
Silicon Laboratories C8051F500DK:
For this lab, we used the SiliconLabs C8051F500DK evaluation board. The board consists of the 8051
microcontroller but with a lot of added features. Some of the features include a 12 bit analog-to-digital
converter, comparators, built in clock (24 MHz), 40 input/output pins and more. All in all, the
C8051F500 is designed to be a stand-alone system on a chip solution, as claimed by the manufacturer.
The C8051F500 board was connected to the PC using the USB Debug Adapter to DEBUG_A on the
target board with the 10-pin ribbon cable as shown in the figure 2.

Fig.2: Hardware Setup of C8051F500 using a USB DEBUG Adapter
The C8051F500 also has two 12-bit Analog to Digital Converters (ADC). We connected one of the
ADCs to the 1k potentiometer via P1.2 pin (A-22 on C8051F500 Female Socket). This allowed us to
easily adjust the desired speed. Also, the PWM generated from the C8051F500 was sent through P0.2
(A-12, CEX0) to the EN1,2 (Enable pin) on the H-Bridge. The pulses generated by the motor amplified
from the op-amp were to be counted using the counter 0. Thus, counter 0 which was counting the pulses
was connected to the pin P0.3 (C-11).


DC Motor:
For this lab, we used a 9V DC motor (model # ESCAP 28 D2R 11) with a built in optical encoder,
consisting of a slotted disk and a phototransistor-LED pair. The phototransistor outputs a pulse whenever
the slot in the disk passes by the phototransistor-LED pair. The phototransistor/LED sensor pair is
powered by 5 volts and generates a sinusoidal signal with 80 counts per turn and 0.2 2V peak-to-peak.
The DC motor itself, is rated at 9V and has a maximum continuous torque of 3.32 oz-in and maximum
continuous current of 1.5A at 1.5 oz-in load. Under normal operating conditions, the motor drains about
650 mA of current at 3000 rotations per minute (RPM).
The motor came with the following six wires whose specifications are mentioned below:
- Motor +, Motor
These two pins connect to the outputs (1Y and 2Y) of the H-bridge chip and supply the motor
with current from the H-bridge chip.
- Collector
This pin connects to a voltage source of 5V through a 1k resistor to generate a current (max of 5
mA) for the phototransistor in the encoder,
- Anode
This pin connects to a voltage source of 5V through a 220k resistor, so that about 27 mA of
current (max recommended is 30 mA) is allowed to flow through the photodiode in the encoder.
- Emitter, Cathode
Both of these pins were connected to ground.


Fig.3: DC Motor with Encoder


H-Bridge:
In order to allow control of the motor through PWM, we need a motor controller chip. For this lab, we
employed the SN754410 IC chip. This chip, like all H-Bridges, inputs PWM and direction and outputs
high voltage power supply to the motors.

Fig.4: Pin Configuration for the SN754410
The chip consists of two separate H-Bridge circuits and can be used to drive up to 2 DC motors. For
directional control, a signal from +5V was connected to pin 2 (1A) and the same +5V signal was put
through an inverter (74LS04) and the output of the inverter was connected to pin 7 (2A). This +5V comes
from a logic switch on the CADET board and flipping the switch will cause the motor to change its
direction of rotation. Pin 1 (1,2 EN) was connected to the PWM output from the C8051F500 board so that
the duty cycle of the motor could be changed. Pins 3 and 6 (1Y and 2Y) were connected to the two
terminals of the DC motor and were the actual pins that powered up the DC motor. Pins 4,5,12 and 13
were all connected to ground. Also, pins 8 (VCC2, to power the motors) and 16 (VCC1, logic supply
voltage) were both connected to +5V. Overall, when an enable input was high, the associated drivers
were enabled and their outputs became active and in phase with their inputs. When the enable input was
low, the drivers were disabled and their outputs were off and in a high-impedance state.
As for the technical specifications, the chip can provide directional current to motors at up to 1A and from
4.5V to 36V. It also has an operating temperature of -40
o
C to 85
o
C.




Signal Conditioning Circuit:
To interface the encoder from the motor with the C8051F500 board, we would need a signal conditioning
circuit that can be used to detect and remove the non-zero DC offset voltage of the signal. Analog
comparators compare two analog voltage values where one input is typically a reference voltage and the
other the unknown value. When the unknown voltage exceeds the reference, the output of the comparator
switches from one binary state to another. A DC-Level detection comparator circuit was built using the
LM741 op amp. The oscilloscope was used to measure the sine waves from the DC motor and an
appropriate reference voltage was determined for detection.
Parts Needed for Conditioning Circuit: LM741 (Op Amp), 1k Potentiometer to adjust the reference
voltage or a voltage divider as shown in the figure 5.

Fig.5: Signal Conditioning Circuit Used

LCD Display:

Fig.6: Regular 16x2 LCD Display
We made use of the LCD display in this lab to display actual and target RPM of the motor. The LCD that
we used was 16x2, meaning it could display an array of characters that was 16 columns by 2 rows (total
32 characters) as shown in fig 6. The LCD display (CFAH1602A-YYH-JPE) had the following pins:

- V
ss
: Ground
- V
dd
: Supply Voltage (5V)
- V
O
: Operating voltage for LCD (for adjusting contrast of display through 10K potentiometer)
- RS: Flag, High for Data, Low for Instruction Code, Connected to P3.0
- R/W: High for Read, Low for Write, Connected to P3.1
- E: Chip Enable, Connected to P3.2
- DB0 => DB7: Data Bit 0 to Data Bit 7, where DB7 is also the busy flag, Connected to P2.0
P2.7, Where busy flag is P2.7
The operating voltage (V
O
) for the LCD contrast was connected to a potentiometer circuit to easily adjust
the contrast (brightness of the pixels of the LCD). It was to be noted that if V
o
was set too high, there
would be a chance of burning out the LCD display. Also, the grounds of the LCD and the supply line (the
CADET board) were shorted. The LCD interacting circuit that was built on the small CADET board is
given in figure 7.

Fig.7: Wiring Diagram for the 16x2 LCD Display

Software:
For this lab, there were many components that were to be used for the software configuration. Initially,
we had to make sure that the LCD display was working properly. After confirming with working LCD
display, PCA initialization to implement PWM generation, implementation of analog to digital
conversion and pulse-counting using a timer as a counter was configured. Finally, to maintain the same
motor speed when load varies, a stable PID feedback control system was designed and employed to
provide acceptable speed control. While configuring the software, the programs for each part were written
in small step and next part was approached only after making sure that previous part was working
perfectly. The whole software configuration for the system was programmed using C8051F500 board, in
C. The details of each part in given below:
LCD Operation
The LCD consists of two registers the Instruction Register (IR), to which commands are sent to control
the LCD actions and Data Register (DR), where the display data is sent. The IR can store instructional
codes such as display clear and cursor shift, and address information for display data RAM (DDRAM)
and character generator RAM (CGRAM). Also, the IR can be programmed from the 8051.
Table 1: Register Selection
RS R/W Enable Operation
0 0 H,H -> L IR- write as internal operation
(Display clear, etc.)
0 1 H Read Busy Flag (DB7) and
Address Counter (DB0-DB6)
1 0 H,H -> L DR => write as internal
operation DR to DDRAM or
CG RAM
1 1 H DR-read as internal operation
(DDRAM or CGRAM to DR)

The IR is chosen by setting the Register Select (RS) to 0 and DR with 1. The LCD is also equipped
with a busy flag (DB7). When it is 1, the LCD is in internal operation mode and the next instruction will
not be accepted. As per the Register selection table, the busy flag is output to DB7 when RS = 0 and R/W
= 1. The next instruction cannot be written unless the busy flag is set to 0.

In our program, a function, LCD_Init() was created. In this function, the busy flag was constantly checked
to ensure that the commands are being accepted by the LCD.

Fig. 8: LCD Busy_flag configuration
Within the LCD_Init() function, a sequence of six steps were performed one after the other. In each step,
the enable was set to high and then to low because the internal operations work only when the enable is
high and there is a high to low transition. The six steps of initialization and the pattern of data bus lines
and select bits are shown below in the table.
Table 2: Instructions and Their Corresponding Codes
DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0 RS R/W E
Clear
Display
0 0 0 0 0 0 0 1 0 0 1->0
Function
Set
0 0 1 1 1 1 0 0 0 0 1->0
Display
Off
0 0 0 0 1 0 0 0 0 0 1->0
Entry
Mode
0 0 0 0 0 1 1 0 0 0 1->0
Display
On
0 0 0 1 1 1 1 1 0 0 1->0
Return
Home
0 0 0 0 0 0 1 0 0 0 1->0
For the checking part, another function,
LCD_busy_flag() was created, which
selected the IR and set the R/W and LCD
Enable to high to read the busy flag from
the IR. The busy flag at line DB7 was read
from P2.7 and returned as an integer
(configured as input).


It is to be noted that before the instructions are executed, the status of the busy flag should be checked so
that it is clear. Given below is the initialization code:
/* --------------------------------------------------
LCD_Init_ISR

setup Timer_0 overflow interrupt service routine
to perform LCD initialization Function
-------------------------------------------------
*/
void LCD_Init_ISR(void) interrupt 1
{char SFRPAGE_SAVE = SFRPAGE;
EA = 0;
TH0 = RTH0;
TL0 = RTL0;
EA = 1;
if(LCD_init_flag == 0)
{
msec_count++;
SFRPAGE = CONFIG_PAGE;

switch(msec_count)
{
case 5:case 10:case 11:case 12:
//function set four times
LCD_en = 1;
RS = 0;
RW = 0;
P2 = 0x3F;
//Pulse enable
LCD_en = 0;
break;

case 13:
//send display off
LCD_en = 1;
RS = 0;
RW = 0;
P2 = 0x08;
//pulse enable
LCD_en = 0;
break;

case 14:
//send display clear
LCD_en = 1;
RS = 0;
RW = 0;
P2 = 0x01;
//pulse enable
LCD_en = 0;
break;








case 16:
//send entry mode set
LCD_en = 1;
RS = 0;
RW = 0;
P2 = 0x06;
// pulse enable
LCD_en = 0;
break;


case 18:
//send display ON
LCD_en = 1;
RS = 0;
RW = 0;
P2 = 0x0F;
//pulse enable
LCD_en = 0;

//set the LCD_init_flag

LCD_init_flag = 1;
break;
default:
break;
} //end of switch-case
} //end of if

if(LCD_init_flag == 1){
SFRPAGE = ACTIVE_PAGE;
TR0 = 0; //disable Timer_0 overflow interrupt, LCD is
initailized
ET0 = 0;
} //end of if

SFRPAGE = SFRPAGE_SAVE;
} //LCD_Init()

Fig. 9: LCD initialization Function using timer 0 overflow interrupt
After the initialization was complete, the LCD needed to be setup for writing. For our application
purposes, we were only writing to the data lines. For displaying, the LCD needs to be refreshed
constantly to update the display. The refreshing was achieved by using Timer 1 Overflow
Interrupt and the advised refresh rate was 50Hz. We also needed to specify the cursor to move to
the next line when the 16
th
character was reached in a row. When the 32
nd
character was
displayed, the LCD was instructed to return to the home position. The position of this cursor was
monitored using a LCD_pointer variable.


Fig.10: The LCD Display Refresh Function

For the actual display of the text, we implemented the following numeric to character data type
function:


Fig. 11: Numeric to Character Data Type Function

As we can see above in figure 11, the TARGET RPM and ACTUAL in RPM were assigned as
four bit signed decimal numbers. In the part of the program with those numbers, we can see the
conversion of numeric data type to character data type.




PWM Generation using Programmable Counter Array (PCA)
PWN is used to control the duty cycle of a square wave, varying between 0% to 100%, and provides the
necessary flexibility for many application. In this lab, the SiliconLab-C8051F500 is being employed to
generate the appropriate PWM signal to control motor rotation speed. The PWM output is connected
directly to an intelligent PWM control H-Bridge chip that will translate the duty cycle information to
drive the DC motor accordingly.
In order to this, C8051F500 provides a Programmable Counter Array (PCA0) consisting of a dedicated
16-bit counter/timer and six 16-bit capture/compare modules. Each capture/compare module has its own
associated I/O line (CEXn), which is routed through the crossbar to port I/O when enabled. For this lab,
we were using capture/compare module 0, where CEX0 was used to generate PWM pulses, while PCA0
timer was driven by the system clock divided by 12. The mode of the PWM was set to 8-bit pulses. Thus,
when the value in the low byte of the PCA counter/timer (PCA0L) was equal to the value in PCA0CPL0,
the PWM signal switches to high and the output on the CEX0 pin will be set. When the count value in
PCA0L overflows, the CEX0 output will be reset. This process will repeat to generate PWM signal with
intended duty cycle. The duty cycle of PWM signal was modulated by changing the value stored in
PCA0CPL0 register as shown:
Setting ECOM0 and PWM0 bits in the PCA0CPM0 register, and setting CLSEL bits in register PCA0PWM to
00b, 8-Bit Pulse Width Modulator mode was enabled using configuration wizard as shown in fig. 12.

Fig. 12: PCA initialization to generate PWM
Initially, the internal oscillator frequency was set at 750000 and we were still able to generate the PWM
pulses. After measuring the frequency of the PWM signal in oscilloscope, we found it to be ~245 Hz.
However, according to Prof. Nings instructions, the H-bridge works best at PWM signal frequency of 1
KHz. Thus, internal oscillator frequency was set to 3 MHz using configuration wizard.


Fig.13: Oscillator Initialization with SYSCLK of 3MHz
As soon as clock was changed from the 0.75MHz to 3 MHz, the LCD display started acting weird as
expected as it was not getting enough time to settle down. Thus, we increased the setting time for the
LCD display refreshing from 25 TIMER-1 overflow interrupts to 100 TIMER-1 overflow interrupts, as
the clock was also changed by a factor of 4.
Hence, in the LCD display refresh function, this part :

was changed to:
However, it was still unable to get to the best condition. Thus, we decided to create same delay in the
LCD Busy_flag function, (fig, 14) which did the trick for us to bring the LCD was back to normal.

Fig. 14: Adding a delay to let LED settle before going to to original position

Analog-Digital Conversion:
As specified in the design contains, the most important design goal of this lab was to Allow the users to
easily adjust the desired motor rpm speed. In order to do so, A/D converter (ADC0 for 12-bit A/D) on the
Silicon-Lab C8051F500 microcontroller was used. The ADC0 on the C8051F50x/F51x consists of an
analog multiplexer (AMUX0) with 35/28 total input selections and a 200 kilo-samples-per-second (ksps),
12-bit successive-approximation-register (SAR) ADC with integrated track-and-hold, programmable
window detector, programmable attenuation (1:2), and hardware accumulator.
In order to initiate the conversion, we chose TIMER 2 overflow interrupt as repeated A/D conversions
can be achieved in fixed intervals. Fig. 12 shows the configuration of TIMER 2 interrupt:

Fig 12: Timer 2 overflow interrupt used to initiate A/D conversion
The conversion mode is determined by the programmed states of the ADC0 bits (AD0CM1, AD0CM0) in
bit-adorable SFR ADC0CN. ADC0CN was set to 11, representing the conversion initialization to TIMER
2 overflow interrupt. AD0BUSY bit in the ADC0CN SFR was set to 1 during conversion and cleared
when conversion complete. The falling edge of the AD0BUSY bit triggers an interrupt and sets the
ADOINT interrupt flag. The converted data then become available in the ADC0H and ADC0L. Output in
our case is right-justified in our case, so the most significant 8 bits of the converted data would reside in
ADC0L and ADC0H would carry the least significant 4 bits.
While configuring ADC0, the example program from the Silicon-Lab library proved to be very helpful.
ADC0 on the C8051F500 implements a selectable gain adjustment option because input sources have
small part-to-part variations that must be accounted for to achieve accurate results and the programmable
gain value could be used as a calibration value to eliminate these part-to-part variations. Thus, gain was
initialized to account for a 5V input and Vref was set to ground. The gain value was calculated using the
following formula, which is represented as a 12-bit frame ranging from 0x000 to 0Xfff.


The gain enable bit [ADC0CF.0] was set and ADC0H were loaded with ADC0GNH, ADC0GNL, or
ADC0GNA addresses. As mentioned earlier, ADC0L was then loaded with the upper bytes GAIN value,
which is 0x6C in this case. However, an ADC conversion should not be performed while the GAINEN bit is set.
Thus the gain bit is set to 0 after proper configuration. ADC0 is then disabled and normal tracking conversion is triggered on
TIMER 2 overflow interrupt by setting ADC0CN to 0x03. P1.2 is used as the analog input pin and ADC0 conversion complete
interrupt was enabled by setting EIE1 to 0x04 and AD0EN to 1. The detailed code is given in fig. 13.

Fig. 13: Analog-digital conversion configuration using A/D converter
Pulse-Counting of actual RPM signal:

In order to find the actual RPM of running DC motor, the pulse counts per second is required to
be measured. In order to measure the pulses, we needed to use a timer that can be used as a counter.
However, as described already, all four timers were already being used. Then we realized that we are not
using TIMER 0 after the LCD display is initialized once. Thus, we decided use TIMER 0 after the LCD
display initialization was completed to count the pulses generated. In order to measure the current RPM
of the motor, we configured Timer 0 as an 8 bit auto-reload counter. In the configuration wizard, we set
T0 to P0.3 in order to record the number of pulses. The pulses, as explained earlier, could not be directly
used by the C8051F120, therefore we had to send the signal through the comparator circuit. The output of
the comparator circuit was then sent to the C8051F120 in order to count the pulses. While loading the
TIMER 2 overflow values, we had to take care of internal oscillator frequency. As mentioned earlier, the
internal oscillator clock used in this system was 3MHz. Thus, when we were loading the values for the
auto-reload mode, we had to make sure that they were properly calculated. The TIMER-2 was configured
as SYSCLK/12, so I took 4 s (1/(3000000/12)) for one machine cycle. Thus, when we trying to measure
pulse-counts per 100 ms, actual delay was supposed to be (100 ms/4) = 25 ms. Hence, (65535-25000 =
40535)
10
= (9E57)
16
was loaded in the TIMER-2 registers as shown in the fig. 14.

else if(LCD_init_flag == 1)
{
EA = 0;
TMR3CN = 0x04; //Start running Timer 3
TMOD = 0x15; //Timer_0 set as event counter and Timer_1 still in 16-bit mode
TH0 = 0x00; //Timer 0 high and low bytes set
TL0 = 0x00;
ET0 = 0; //Disable Timer 0 interrupt, Timer 0 will now be used to count pulses
EIE1 |= 0x40; //Timer 3 interrupt enabled and used to record pulses counted by Timer0
TMR3L= 0x57; //Timer 3 high and low bytes set to record pulse every 100 msec
TMR3H = 0x9E;
TMR3RLL = 0x57; //Timer 3 auto reload values set
TMR3RLH = 0x9E;
EA = 1;}
Fig. 14: Configuration of TIMER 0 and TIMER 2 overflow interrupt


After configuring TIMER 0 as a counter to count pulses and TIMER 3 overflow interrupt to record the
counted pulses, which is equal to (total_pulse_count = TH0*256 + TL0). The total_pulse_count is then
used to measure the actual speed of the running DC motor in rpm using the following formula.




where total_pulse_count is in x-msec.
As suggested by prof. Ning, mechanical devices like DC motor is not fast enough to adjust the speed in
very small time frame like 100 msec. Thus, we decided to record the pulses after 500 ms or second. A
small counter was used to count the 100 ms delay set by TIMER 3 overflow interrupt and after such 5
interrupt, total_pulses_counts were recorded using the above equation.
However, after trying multiple combinations of TIMER-3 overflow delay, we were not getting the result
expected. Then we decided to measure the frequency of the pulses recorded and we noticed the counter in
the TIMER-3 overflow interrupt subroutine was not going through. After going through the manual, we
got to know that in TMR3CN (Timer 3 control) register, bit TF3H and bit TF3L are not automatically
cleared by the hardware (fig. 15). Therefore, when first TIMER-3 overflow happened, the timer was not
going back to reset and was not running again. In order to do so, we had to clear TF3H and TF3L to start
the timer again. Hence, the TIMER-3 was enabled after each overflow interrupt.

Fig. 15: TMR3CN: Timer 3 Control

After enabling TIMER-3 again in TIMER-3 overflow interrupt subroutine, we were getting the results
expected. However, though we were getting better result, the values were still fluctuating so much. Thus,
we decided to record the pulses after 1000ms or 1 second. Therefore, after 10 TIMER-2 overflow
interrupts (each with 100ms), total_pulses_counts were recorded and used to find current speed of motor
in rpm as shown in following equation. Fig. 16 gives details about TIMER-3 overflow interrupt
subroutine.


----------------------------------------------------------
Timer2_Overflow_ISR
The Routine will count the number of pulses. The number
of pulses comes from TL0 and TH0 because Timer 0 was set to
record pulses
----------------------------------------------------------
*/
void Timer3_Overflow_ISR(void) interrupt 13 //Interrupts every 100msec (auto reload)
{
TMR3CN = 0x04; //Timer-3 is enabled again
ISRT3_count++;
if (ISRT3_count == 10) //refreshes actual_rpm every 100*10 = 1000 msec
{
pulse_count = TH0*256 + TL0; // the number of pulses
TH0 = 0x00; //Timer 0 high and low bytes reset
TL0 = 0x00;
//Routine convert pulses into RPM here
//rpm = pulse_count(1/1000msec)(1000msec/1sec)(60sec/1min)(1turn/80counts)
actual_rpm = pulse_count * 0.75;
ISRT3_count = 0;
target_rpm = ADC0;
FEEDBACK_Init();
}}
Fig. 16: Timer -3 Overflow Interrupt Service Routine


After configuring the LCD display, PWM generation using Programmable Counter Array (PCA), desired
speed target using A/D converter and calculating the actual rpm, everything was placed together to check
the whole embedded system. We were able to change the desired target speed and thus, the actual speed
of the motor. The change in the target speed and actual speed was successfully displaced on the LCD
display. However, there was no proper correlation between the desired target speed and actual speed. In
order to adjust the PWM signal to achieve a desired target speed a feedback controller was necessary.
Thus, feedback control was designed using the difference between the desired target speed and the actual
speed of the motor. Thus, when the desired target speed was different than the actual speed, duty cycle of
the PWM signal was adjusted accordingly by the controller to maintain the desired speed. When target
speed was higher than actual speed of the motor, duty cycle of PWM signal was increased accordingly by
decreasing the value of PCA0CPH0 in programmable counter array (PCA) and vice versa.
/*--------------------------------------------------------
Feedback
--------------------------------------------------------*/
void FEEDBACK_Init(void)
{ rpm_change = target_rpm - actual_rpm
if(rpm_change > 0 && PCA0CPH0 > 0x05)
{PCA0CPH0 = PCA0CPH0 - 0x01;}
else if(rpm_change < 0 && PCA0CPH0<0xFF)
{PCA0CPH0 = PCA0CPH0 + 0x01;}
Else {PCA0CPH0 = PCA0CPH0;}
}
Fig. 17: Initial approach to control the
Initially we wrote a simple program (fig. 17), which will increase duty cycle if the difference between
desired target speed and actual speed is higher than 0 and will decrease the duty cycle if difference
between desired target speed and actual speed is less than zero given that the duty cycle is between 0%
and 100%. However, the feedback was not responsive enough and it took so long for the actual speed to
adjust accordingly to the target speed. Furthermore, when we tried to change the incrementing value of
PCA capture module 0 high byte (PCA0CPH0), the actual speed started fluctuating. Thus, we decided to
keep checking the difference between target rpm and actual rpm constantly and then change the value of
PCA0CPH0 accordingly. The difference between target rpm and actual rpm was being checked from 0 to
1200 and appropriate change in PCA0CPH0 was placed. After employing the change in the feedback
control loop, our motor started adjusting the actual speed very quickly according the desired target speed.
In addition, by removing the load or increasing the load, the duty cycle was quickly being automatically
adjusted accordingly by the controller to maintain the desired speed. Furthermore, the actual speed was
not fluctuating and will stay within 15-20 rpm of target speed. This was also verified from the stable
actual speed in rpm and PWM signal from the oscilloscope. The final working code for the feedback
control is given in fig. 18.

/*--------------------------------------------------------
Feedback Control
--------------------------------------------------------*/
void FEEDBACK_Init(void)
{rpm_change = target_rpm - actual_rpm;
//If desired target speed is higher than actual speed of motor if duty cycle is less than 100%
if((rpm_change>0) && (PCA0CPH0> 0x00))
{
if((rpm_change>=1200))
{ PCA0CPH0 = PCA0CPH0 - 0x05;}
else if((rpm_change>=1000) && (rpm_change<=1200))
{ PCA0CPH0 = PCA0CPH0 - 0x05;}
else if((rpm_change>=800) && (rpm_change<=1000))
{ PCA0CPH0 = PCA0CPH0 - 0x04;}
else if((rpm_change>=500) && (rpm_change<=800))
{ PCA0CPH0 = PCA0CPH0 - 0x03;}
else if((rpm_change>=300) && (rpm_change<=500))
{ PCA0CPH0 = PCA0CPH0 - 0x02;}
else if((rpm_change>=100) && (rpm_change<=300))
{ PCA0CPH0 = PCA0CPH0 - 0x01;}
else if((rpm_change>=50) && (rpm_change<=100))
{ PCA0CPH0 = PCA0CPH0 - 0x01;}
else if((rpm_change>10) && (rpm_change<=50))
{ PCA0CPH0 = PCA0CPH0 - 0x01;}
else if((rpm_change>0) && (rpm_change<=10))
{ PCA0CPH0 = PCA0CPH0;}
else {PCA0CPH0 = PCA0CPH0;}
}

//If desired target speed is lower than actual speed of motor if duty cycle is higher than 0%
else if((rpm_change<0) && (PCA0CPH0< 0xFF))
{
if((rpm_change<=-1200))
{ PCA0CPH0 = PCA0CPH0 + 0x05;}
else if((rpm_change<=-1000) && (rpm_change>=-1200))
{ PCA0CPH0 = PCA0CPH0 + 0x05;}
else if((rpm_change<=-800) && (rpm_change>=-1000))
{ PCA0CPH0 = PCA0CPH0 + 0x04;}
else if((rpm_change<=-500) && (rpm_change>=-800))
{ PCA0CPH0 = PCA0CPH0 + 0x03;}
else if((rpm_change<=-300) && (rpm_change>=-500))
{ PCA0CPH0 = PCA0CPH0 + 0x02;}
else if((rpm_change<=-100) && (rpm_change>=-300))
{ PCA0CPH0 = PCA0CPH0 + 0x01;}
else if((rpm_change<=-50) && (rpm_change>=-100))
{ PCA0CPH0 = PCA0CPH0 + 0x01;}
else if((rpm_change<=-10) && (rpm_change>=-50))
{ PCA0CPH0 = PCA0CPH0 + 0x01;}
else if((rpm_change<=0) && (rpm_change>=-10))
{ PCA0CPH0 = PCA0CPH0;}
else {PCA0CPH0 = PCA0CPH0;}
}
else {PCA0CPH0 = PCA0CPH0;}
}
Fig. 18: Feedback control loop to adjust actual speed according to desired target speed

Implementation
As mentioned already, there were many components that were to be employed for the hardware as well as
software configuration. Thus, while configuring the whole system, each part was configured in small step
and next part was approached only after making sure that previous part was working perfectly. Initially,
given LCD display was properly wired using the wiring diagram given in fig.7. It was a good advantage
for us that an almost working code for the LCD was provided. Modifying the code a bit, we were able to
make our LCD display desired output. While I was working on the programming, Bicky finished building
the circuitry consisting of H-bridge, DC motor with optical encoder, comparator, inverter and op-amp.
With the LCD screen working with one set of code, we referenced the PWM example code given in the
lab handout. This helped us to set up PCA and to generate varying duty cycles of the PWM. Before we
hooked up the H-Bridge, we connected the PCA directly to the Port 0.2 (CEX0), which was configured
using the configuration wizard. In order to obtain a stable PWN signal, we needed to measure the actual
speed of the DC motor. Thus, timer 0 was configured as 16-bit counter while timer-3 overflow interrupt
was used for count the pulses measured by counter 0. However, the pulses we obtained from the PWM
were not being counted by Timer 0. To increase the current of the PWM pulses, we passed the output of
the comparator through a buffer. This solved the problem and enabled the timer to count the pulses.
Working with the PWM code, we then passed the PWM to the input of the H-Bridge to power the motor.
When we connected the pulses from the motor to the oscilloscope, we were able to get expected signal.
The desired rpm speed was then set using the Analog to Digital Converter with help of example code
given in the SILABs documentation. We used GND as reference voltage for VREF (P0.0). Using the
value of total-pulses obtained using counter 0 and TIMER-3 overflow interrupt, actual rotating spped was
found. Using actual and target speed, feedback control loop was finally employed to adjust actual speed
of the motor accordingly to the desired target speed. Multiple tests were performed to design the best
possible feedback control system.
When we thought we were almost done one week ago, we decided to check the progress with professor.
Then we realized that the H-bridge works best with the oscillator frequency that leads to the PWM signal
frequency of 1KHz. Hence, we had to change our clock from 0.75 MHz to 3 Mhz. This change brought us
back to the same place where we started. Nothing including LCD display, actual-rpm courting or
feedback-control was working. Three-four days were spent without any significant progress. However,
things started to be on track after we figured out the timing conflict with the delays in the timer overflow
interrupt due to the change in oscillator. After multiple tests with different values of almost every single
variable, the DC motor speed control embedded system was finalized.

Results and Discussion
As per the design goal of the lab, we were able to construct DC-motor based embedded system, where
PID feedback controller was designed that would allow the user to enter the desired target speed and
timely adjustment to the PWM would be made to accommodate the changing shaft-load to maintain the
desired rotating speed. As mentioned above in the fig.17, while demonstrating the lab to professor, the
feedback control was designed to slowly adjust the actual rotating speed of the motor according to desired
target speed. However, after the professors concern, we implemented the feedback using the code given
in fig.18, which was able to make quick adjustment in the actual rotating speed according to the target
speed when load was applied.
This lab opened the doors for what we can do with the 8051-based microcontrollers. While learning about
the Automatic Control System (ACS), I never thought that I could even use it literally. However, this lab
demonstrated practical ways of implementing closed-loop feedback systems. Furthermore, this lab also
helped a lot to improve our command on C/C++ language.















//____________________________________________________________________
//LAB 4: DC motor control using PWM and PID Controller
//Vishal Bharam
//Bicky Shakya
//Engr 323: MicroProcessor Systems
//Professor: Dr. Taikang Ning
//____________________________________________________________________
/*
This program is written to control the speed of the DC motor based on the feedback
from the C8051F500 microcontroller. First, the ADC converts the signal from the
potentiometer. Due to various reasons, the desired rpm speed and the current rpm speed
may not be equal. Therefore the program counts the number of pulses within a certain
time period.Then the current rpm speed of the DC motor is calculated. The program takes the
difference between the two values and adjusts the duty cycle of the PWM signal accordingly.
The current rpm speed as wells as the desired rpm speed will be displayed on the LCD.
*/

//-----------------------------------------------------------------------------
// Includes
//-----------------------------------------------------------------------------
#include "compiler_defs.h"
#include "C8051F500_defs.h"
#include <stdio.h>

//-----------------------------------------------------------------------------
// Function Prototypes
//-----------------------------------------------------------------------------
void Oscillator_Init(void);
void Port_Init(void);
void Timer_Init(void);
void LCD_Init_ISR(void);
void Display_String(void);
void ADC_Init(void);
void PCA_Init(void);
void TIMER2_Init(void);
void DISPLAY_ISR(void);
int LCD_busy_flag (void);
void FEEDBACK_Init(void);
void AD0_ISR(void);
void Timer3_Overflow_ISR(void);

//-----------------------------------------------------------------------------
// Global Constants
//-----------------------------------------------------------------------------
#define SYSCLK 3000000
sbit LED = P1^6;
sbit RS = P3^0;
sbit RW = P3^1;
sbit LCD_en = P3^2;
sbit DB7 = P2^7;

//-----------------------------------------------------------------------------
// Global Variables
//-----------------------------------------------------------------------------
char xdata LCD_display[32]; //hold the data to be shown on an LCD
unsigned char LCD_pointer = 0;
char indicator;
char LCD_init_flag = 0;

short msec_count = 0; //each delay is about 1 msec
short RTH0, RTL0, RTH1, RTL1; //reload values for Timer_0 and Timer_1
short i, k;
short target_rpm = 500;
short actual_rpm = 0;
short pulse_count = 0;
short count = 0;
short ISRT3_count = 0;
short rpm_change;


//-----------------------------------------------------------------------------
// main() Routine
//-----------------------------------------------------------------------------

void main(void) //calling functions here
{
PCA0MD &= ~0x40; // Disable the watchdog timer
LCD_Init_ISR();
Oscillator_Init();
Port_Init();
Timer_Init();
PCA_Init();
ADC_Init();
TIMER2_Init();
DISPLAY_ISR();
FEEDBACK_Init();
Display_String();

IP= 0x2A; //timer 1,2,3 prioritized
while(1);
}



//-----------------------------------------------------------------------------
// Initialization Subroutines
//-----------------------------------------------------------------------------

/*--------------------------------
Oscillator_Init
This routine initializes the system clock to use
the precision internal oscillator as its clock
source
---------------------------------------------------
*/

void Oscillator_Init(void)
{
SFRPAGE = CONFIG_PAGE;

OSCICN = 0xC4; //Select the internal oscillator as system clock 12.25 MHz
CLKSEL = 0x00; //Select the internal oscillator as SYSCLK source

SFRPAGE = ACTIVE_PAGE;
}

/*-------------------------------------------------------
PORT_Init()
-------------------------------------------------------*/

void Port_Init(void)
{
char SFRPAGE_SAVE = SFRPAGE;
SFRPAGE = CONFIG_PAGE;

P0SKIP |= 0x03; // Skip P0.0 (VREF)
P0MDIN = 0xFC;
P0MDOUT = 0x0C; // Set External Timer 0 (P0.0) to push-pull
XBR1= 0x44; //CEX0 at port0.2, timer0 pin at p0.1
P1SKIP |= 0x04; // Skip P1.2 (ADC input)
P1MDIN &= ~0x04; // Set P1.2 as an analog input
P1MDOUT |= 0x08; //P1.6(LED)
P3MDOUT = 0x07; //LCD-en(P6.2) RS(P6.0) and RW(P6.1) are set
P2MDOUT = 0xFF; //DB0-DB7 are set push-pull
XBR2 = 0x40; //Enable Crossbar

SFRPAGE = SFRPAGE_SAVE;
}

//-----------------------------------------------------------------------------
// TIMER2_Init
//-----------------------------------------------------------------------------
//
// Return Value: None
// Parameters: None
//
// Configure Timer2 to 16-bit auto-reload and generate an interrupt at 100uS
// intervals. Timer 2 overflow automatically triggers ADC0 conversion.
//
//-----------------------------------------------------------------------------

void TIMER2_Init (void)
{
U8 SFRPAGE_save = SFRPAGE;
SFRPAGE = ACTIVE_PAGE;
TMR2CN = 0x00; // Stop Timer2; Clear TF2;
// use SYSCLK as timebase, 16-bit auto-reload
TMR2RL = 65535 - (SYSCLK / 10000); // Init reload value for 10uS
TMR2 = 0xFFFF; // Set to reload immediately
TR2 = 1; // Start Timer2

SFRPAGE = SFRPAGE_save;
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// ADC_Init
//-----------------------------------------------------------------------------
//
// Return Value: None
// Parameters: None
//
// Configures ADC0 to make single-ended analog measurements on pin P1.2
// Also enabled the gain and the voltage reference
//
//-----------------------------------------------------------------------------

void ADC_Init()
{
U8 SFRPAGE_save = SFRPAGE;
SFRPAGE = ACTIVE_PAGE;

// Initialize the Gain to account for a 5V input and 2.25 VREF
// Solve the equation provided in Section 9.3.1 of the Datasheet


// The 5V input is scaled by a factor of 0.44 so that the maximum input
// voltage seen by the pin is 2.2V

// 0.44 = (GAIN/4096) + GAINADD * (1/64)

// Set GAIN to 0x6CA and GAINADD to 1
// GAIN = is the 12-bit word formed by ADC0GNH[7:0] ADC0GNL[7:4]
// GAINADD is bit ADC0GNA.0

ADC0CF |= 0x01; // Set GAINEN = 1
ADC0H = 0x04; // Load the ADC0GNH address
ADC0L = 0x6C; // Load the upper byte of 0x6CA to
// ADC0GNH
ADC0H = 0x07; // Load the ADC0GNL address
ADC0L = 0xA0; // Load the lower nibble of 0x6CA to
// ADC0GNL
ADC0H = 0x08; // Load the ADC0GNA address
ADC0L = 0x01; // Set the GAINADD bit
ADC0CF &= ~0x01; // Set GAINEN = 0

ADC0CN = 0x03; // ADC0 disabled, normal tracking,
// conversion triggered on TMR2 overflow
// Output is right-justified

REF0CN = 0x33; // Enable on-chip VREF and buffer
// Set voltage reference to GND

ADC0MX = 0x0A; // Set ADC input to P1.2

ADC0CF = ((SYSCLK / 3000000) - 1) << 3; // Set SAR clock to 3MHz

EIE1 |= 0x04; // Enable ADC0 conversion complete int.

AD0EN = 1; // Enable ADC0
SFRPAGE = SFRPAGE_save;
}


/*----------------------------------------------------
Timer_Init();

set Timer_0 and Timer_1 as 16 bit
-----------------------------------------------------
*/

void Timer_Init(void)
{
char SFRPAGE_SAVE = SFRPAGE;
SFRPAGE = ACTIVE_PAGE;
TMOD = 0x11; //Timer_0 and Timer_1 in 16-bit mode
RTL0 = 0x17; //1000 cycles for about 1 msec
RTH0 = 0xFC;
TR0 = 1;
ET0 = 1;

RTL1 = 0x77; //refresh rate of 100Hz (10 msec)
RTH1 = 0xF8;
TR1 = 1;
ET1 = 1;

EA = 1; //EA and TF0,TF1, and EA are enabled

SFRPAGE = SFRPAGE_SAVE;

}

/*------------------------------------------------------
PWM COUNTER
------------------------------------------------------*/
void PCA_Init(void)
{
PCA0CN = 0x40;
PCA0CPM0 = 0x42;
PCA0CPH0 = 0x80;
}

/* --------------------------------------------------
LCD_Init_ISR

setup Timer_0 overflow interrupt service routine
to perform LCD initialization Function
-------------------------------------------------*/

void LCD_Init_ISR(void) interrupt 1
{
if(LCD_init_flag == 0)
{
EA = 0;
TH0 = RTH0;
TL0 = RTL0;
EA = 1;


msec_count++;
switch(msec_count)
{
case 5:case 10:case 11:case 12:
//function set four times
LCD_en = 1;
RS = 0;
RW = 0;
P2 = 0x3F;
// pulse enable
LCD_en = 0;
break;

case 13:
//send display off
LCD_en = 1;
RS = 0;
RW = 0;
P2 = 0x08;
//pulse enable
LCD_en = 0;
break;

case 14:
//send display clear
LCD_en = 1;
RS = 0;
P2 = 0x01;
//pulse enable
LCD_en = 0;
break;

case 16:
//send entry mode set
LCD_en = 1;
RS = 0;
RW = 0;
P2 = 0x06;
//pulse enable
LCD_en = 0;
break;

case 18:
//send display ON
LCD_en = 1;
RS = 0;
RW = 0;
P2 = 0x0F;
//pulse enable
LCD_en = 0;

//set the LCD_init_flag
LCD_init_flag = 1;
break;

default:
break;
} //end of switch-case


} //end of if
else if(LCD_init_flag == 1)
{
EA = 0;
TMR3CN = 0x04; //Start running Timer 2
TMOD = 0x15; //Timer_0 set as event counter and Timer_1 still in 16-bit mode
TH0 = 0x00; //Timer 0 high and low bytes set
TL0 = 0x00;
ET0 = 0; //Disable Timer 0 interrupt, Timer 0 will now be used to count pulses and Timer 2
records

EIE1 |= 0x40; //Timer 2 interrupt enabled
TMR3L = 0x57; //Timer 2 high and low bytes set to record pulse count every 5 msec
TMR3H = 0x9E;
TMR3RLL = 0x57; //Timer 2 auto reload values set
TMR3RLH = 0x9E;
EA = 1;
}

// SFRPAGE = SFRPAGE_SAVE;
} //LCD_Init()












/* ---------------------------------------------------

LCD Display Refresh Function

---------------------------------------------------
*/

void DISPLAY_ISR(void) interrupt 3
{
//EA = 0;
TL1 = RTL1; //Refresh rate of 50Hz
TH1 = RTH1; //20-msec interval
Display_String();

//The program will not refresh the LCD until the intialization is complete
if((LCD_init_flag == 1) && (LCD_busy_flag() == 0))
{
switch(LCD_pointer)
{

case 16:
LCD_en = 1;
RS = 0;
RW = 0;
P2 = 0xC0; //set DD RAM address to 40H (A7 = 1 100 0000)
LCD_en = 0;

for(k=0;k<100;k++); //delay to let LCD settle
break;


case 32:
LCD_en = 1;
RS = 0;
RW = 0;
P2 = 0x02; //move cursor to home position
LCD_en = 0;
LCD_pointer = 0;

for(k=0;k<100;k++); //delay to let LCD settle
break;

default:
break;
} //end of switch


//Display the LCD data
LCD_en = 1;
RS = 1;
RW = 0;

P2 = LCD_display[LCD_pointer];
LCD_en = 0;

LED = ~LED;
LCD_pointer++;
}
}

/*
----------------------------------------------------------
Timer2_Overflow_ISR

The Routine will count the number of pulses. The number
of pulses comes from TL0 and TH0 because Timer 0 was set to
record pulses
----------------------------------------------------------
*/

void Timer3_Overflow_ISR(void) interrupt 13 //Interrupts every 5 msec and auto reload
{
TMR3CN = 0x04;
ISRT3_count++;

if (ISRT3_count == 10) //refreshes actual_rpm every 1000 msec
{
pulse_count = TH0*256 + TL0; // the number of pulses
TH0 = 0x00; //Timer 0 high and low bytes reset
TL0 = 0x00;

//Routine convert pulses into RPM here
//rpm = pulse_count(1/1000msec)(1000msec/1sec)(60sec/1min)(1turn/80counts)
actual_rpm = pulse_count * 0.75;

ISRT3_count = 0;
target_rpm = ADC0;
FEEDBACK_Init();
}}




/*--------------------------------------------------------
Feedback Control
--------------------------------------------------------*/
void FEEDBACK_Init(void)
{rpm_change = target_rpm - actual_rpm;

//If desired target speed is higher than actual speed of motor if duty cycle is less than 100%
if((rpm_change>0) && (PCA0CPH0> 0x00))
{
if((rpm_change>=1200))
{ PCA0CPH0 = PCA0CPH0 - 0x05;}
else if((rpm_change>=1000) && (rpm_change<=1200))
{ PCA0CPH0 = PCA0CPH0 - 0x05;}
else if((rpm_change>=800) && (rpm_change<=1000))
{ PCA0CPH0 = PCA0CPH0 - 0x04;}
else if((rpm_change>=500) && (rpm_change<=800))
{ PCA0CPH0 = PCA0CPH0 - 0x03;}
else if((rpm_change>=300) && (rpm_change<=500))
{ PCA0CPH0 = PCA0CPH0 - 0x02;}
else if((rpm_change>=100) && (rpm_change<=300))
{ PCA0CPH0 = PCA0CPH0 - 0x01;}
else if((rpm_change>=50) && (rpm_change<=100))
{ PCA0CPH0 = PCA0CPH0 - 0x01;}
else if((rpm_change>10) && (rpm_change<=50))
{ PCA0CPH0 = PCA0CPH0 - 0x01;}
else if((rpm_change>0) && (rpm_change<=10))
{ PCA0CPH0 = PCA0CPH0;}
else {PCA0CPH0 = PCA0CPH0;}
}

//If desired target speed is lower than actual speed of motor if duty cycle is higher than 0%
else if((rpm_change<0) && (PCA0CPH0< 0xFF))
{
if((rpm_change<=-1200))
{ PCA0CPH0 = PCA0CPH0 + 0x05;}
else if((rpm_change<=-1000) && (rpm_change>=-1200))
{ PCA0CPH0 = PCA0CPH0 + 0x05;}
else if((rpm_change<=-800) && (rpm_change>=-1000))
{ PCA0CPH0 = PCA0CPH0 + 0x04;}
else if((rpm_change<=-500) && (rpm_change>=-800))
{ PCA0CPH0 = PCA0CPH0 + 0x03;}
else if((rpm_change<=-300) && (rpm_change>=-500))
{ PCA0CPH0 = PCA0CPH0 + 0x02;}
else if((rpm_change<=-100) && (rpm_change>=-300))
{ PCA0CPH0 = PCA0CPH0 + 0x01;}
else if((rpm_change<=-50) && (rpm_change>=-100))
{ PCA0CPH0 = PCA0CPH0 + 0x01;}
else if((rpm_change<=-10) && (rpm_change>=-50))
{ PCA0CPH0 = PCA0CPH0 + 0x01;}
else if((rpm_change<=0) && (rpm_change>=-10))
{ PCA0CPH0 = PCA0CPH0;}
else {PCA0CPH0 = PCA0CPH0;}
}
else {PCA0CPH0 = PCA0CPH0;}
}

/*--------------------------------------------------------
ADConversion complete interrupt
--------------------------------------------------------*/
void AD0_ISR(void) interrupt 9 // End Of Conversion
{
AD0INT = 0;
}
/*--------------------------------------------------------
LCD busy_flag
----------------------------------------------------------*/

int LCD_busy_flag(void)
{
unsigned char busy;
char SFRPAGE_save = SFRPAGE;
SFRPAGE = CONFIG_PAGE;

P2MDOUT = 0x00; //set PORT for read
P2 = 0xFF;

// the while loop continues until DB7 == 0
while(DB7 != 0)
{
LCD_en = 1;
RS = 0;
RW = 1;
LCD_en = 0;
for(i=0;i<100;i++); //delay to let LCD settle
DB7 = 0; //break out of while loop
}
busy = 0x00;
P2MDOUT = 0xFF;

SFRPAGE = SFRPAGE_save;
return(busy); //Return from interrupt
}



/*----------------------------------------------------------
Display_String()

Initializes the LCD array [32]
----------------------------------------------------------*/
void Display_String(void)
{
//first line display data
LCD_display[0] = 'T';
LCD_display[1] = 'A';
LCD_display[2] = 'R';
LCD_display[3] = 'G';
LCD_display[4] = 'E';
LCD_display[5] = 'T';
LCD_display[6] = ' ';
LCD_display[7] = 'R';
LCD_display[8] = 'P';
LCD_display[9] = 'M';
LCD_display[10] = '-';
LCD_display[11] = ' ';

//generate the rpm number
LCD_display[12] = '0'+target_rpm/1000;
LCD_display[13] = '0'+(target_rpm%1000)/100;
LCD_display[14] = '0'+((target_rpm%1000)%100)/10;
LCD_display[15] = '0'+((target_rpm%1000)%100)%10;

//second line display data
LCD_display[16] = 'A';
LCD_display[17] = 'C';
LCD_display[18] = 'T';
LCD_display[19] = 'U';
LCD_display[20] = 'A';
LCD_display[21] = 'L';
LCD_display[22] = ' ';
LCD_display[23] = 'R';
LCD_display[24] = 'P';
LCD_display[25] = 'M';
LCD_display[26] = '-';
LCD_display[27] = ' ';

//generate the rpm number
LCD_display[28] = '0'+actual_rpm/1000;
LCD_display[29] = '0'+(actual_rpm%1000)/100;
LCD_display[30] = '0'+((actual_rpm%1000)%100)/10;
LCD_display[31] = '0'+((actual_rpm%1000)%100)%10;
}

You might also like