Introduction

Heterogeneous system on chips like the Zynq are ideal for motor control applications. The programmable logic means we can implement complex control algorithms and interface with both industry standard and bespoke interfaces.

As I go to a lot of conferences and exhibitions I wanted to create a compact motor control application which show cased how the Zynq can be used for this.

I wanted this demo to be compact and demonstrate the power of the Zynq, even when using one of the smaller Zynq devices. I therefore decided to use the MiniZed which uses the smallest of the single core Zynq devices.

The architecture of the design will be the following.

MiniZed

Multi Touch Display Screen

Pmod HB3

12V DC Motor

Controlling the motor is fairly straight forward, to enable a logic level FPGA output to supply a higher voltage needed by most DC motors we use a circuit called a H-Bridge.

The H-Bridge allows us to supply not only a higher voltage but also to be able to control the direction of the motor depending upon a direction signal.

H-bridge in reverse direction

H-Bridge in forward direction

The simplest FPGA H-Bridge control circuit uses two digital control lines, one for the direction and the second to enable the voltage supply to the motor. While this approach works, it provides a very digital motor control, either the motor is running at its full voltage or it is off.

Of course when the maximum voltage is applied to the motor the speed of rotation will be at its maximum. In our applications we need to be able

A more granular control of the motor can be achieved using Pulse Width Modulation (PWM). PWM enables us to control the average voltage across the motor by switching the voltage on and off with a set period repeated pattern.

Increasing the duty cycle results in a higher average voltage across the motor and hence a higher motor speed. Similarly decreasing the duty cycle reduces the average voltage across the motor and lowers the motor speed.

This provides a very granular control of the motor speed.

PWM example waveform

Hardware Build

The hardware design created in Vivado, as we are using the MiniZed we will create a project which uses the MiniZed as the target board.

Setting the MiniZed as the target for the project

Project summary

Into the new project we are going to add in a Zynq processing system and run the block automation to configure the processing system correctly for the MiniZed.

Adding in the processing system

Block automaton settings

Once we have configured the processing system for the MiniZed we need to customise the processing system for our application. In this case we need to enable the Master General Purpose AXI interface from the processing system to the programmable logic.

Enabling the Master GP AXI interface

To this we will be adding in the following IP

Pmod MTDS

Once this is added we can run the connection automation, to connect the PmodMTDS into the processing system

Adding the IP blocks

Running the connection automation

As we are using the PmodMTDS to the arduino shield connector, we need to re customise the PmodMTDS IP block to use the SPI and not Pmod.

Setting the SPI

The PmodMTDS will allow us to have display and control the motor, what we need to do now is implement the direction signal and the PWM output.

To do this we will be using elements within the processing system itself, for the direction we will be using a GPIO signal routed to the EMIO. The PWM signal itself will be provided by the Triple Timer Counter.

Each TTC contains three timers, each of which is able to generate a PWM output. Within the Zynq PS we have two TTCs.

This means we are able to generate up to six PWM signals from the processing system.

If we want to control more than six motors using PWM, we can implement additional PWM IP functions in the programmable logic.

To ensure the PmodMTDS can function correctly we also need to enable interrupts from the programmable logic to the processing system

Enabling the PL to PS Interrupts

Enabling GPIO via EMIO

Enabling the timer and routing the wave output to the EMIO

Once all of this is completed in Vivado the block diagram should look as below.

Completed Block Diagram

Once the build is completed we can export the design to SDK and generate the software application

Software Application

Inside SDK once the hardware platform has been imported, the next step is to create a Board Support Package.

When the dialog is shown to customise the BSP ensure the stdin / stdout are set to ps7_uart_1

BSP settings

Once the BSP is generated, we are ready to create the application project this will be a C++ project.

Creating the application project

The software architecture of the design is as follows:

Configuration of the display and enabling buttons

Set up the TTC for PWM output

Enable the interrupt controller and set up the TTC interrupt

Create the TTC interrupt service routine

Create the main application

Within the main application, there will be 9 buttons, six of the buttons will provide different speeds while the remaining three will set the direction or stop the motor.

Setting up the tick once the interrupt controller and timer are configured:

int SetupTicker(XTtcPs *TtcPsInst,u16 DeviceID,u16 TtcTickIntrID,XScuGic *InterruptController) { int Status; TmrCntrSetup *TimerSetup; XTtcPs *TtcPsTick; TimerSetup = &SettingsTable; TimerSetup->Options |= (XTTCPS_OPTION_INTERVAL_MODE | XTTCPS_OPTION_MATCH_MODE | XTTCPS_OPTION_WAVE_POLARITY); Status = SetupTimer(DeviceID,TtcPsInst); if(Status != XST_SUCCESS) { return Status; } TtcPsTick = TtcPsInst; Status = XScuGic_Connect(InterruptController, TtcTickIntrID, (Xil_InterruptHandler)TickHandler, (void *)TtcPsTick); if (Status != XST_SUCCESS) { return XST_FAILURE; } XScuGic_Enable(InterruptController, TtcTickIntrID); XTtcPs_EnableInterrupts(TtcPsTick, XTTCPS_IXR_INTERVAL_MASK); XTtcPs_Start(TtcPsTick); return Status; }

Setting up the interrupt controller:

static int SetupInterruptSystem(u16 IntcDeviceID,XScuGic *IntcInstancePtr) { int Status; XScuGic_Config *IntcConfig; IntcConfig = XScuGic_LookupConfig(IntcDeviceID); if (NULL == IntcConfig) { return XST_FAILURE; } Status = XScuGic_CfgInitialize(IntcInstancePtr, IntcConfig, IntcConfig->CpuBaseAddress); if (Status != XST_SUCCESS) { return XST_FAILURE; } Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT, (Xil_ExceptionHandler) XScuGic_InterruptHandler, IntcInstancePtr); Xil_ExceptionEnable(); return XST_SUCCESS; }

Setting up the TTC:

int SetupTimer(u16 DeviceID,XTtcPs *TtcPsInst) { int Status; XTtcPs_Config *Config; XTtcPs *Timer; TmrCntrSetup *TimerSetup; TimerSetup = &SettingsTable; Timer = TtcPsInst; Config = XTtcPs_LookupConfig(DeviceID); if (NULL == Config) { return XST_FAILURE; } Status = XTtcPs_CfgInitialize(Timer, Config, Config->BaseAddress); if (Status != XST_SUCCESS) { return XST_FAILURE; } XTtcPs_SetOptions(Timer, TimerSetup->Options); XTtcPs_CalcIntervalFromFreq(Timer, TimerSetup->OutputHz, &(TimerSetup->Interval), &(TimerSetup->Prescaler)); XTtcPs_SetInterval(Timer, TimerSetup->Interval); XTtcPs_SetPrescaler(Timer, TimerSetup->Prescaler); return XST_SUCCESS; }

TTC interrupt service routine:

static void TickHandler(void *CallBackRef) { u32 StatusEvent; StatusEvent = XTtcPs_GetInterruptStatus((XTtcPs *)CallBackRef); XTtcPs_ClearInterruptStatus((XTtcPs *)CallBackRef, StatusEvent); }

Main program:

#include "xil_cache.h" #include "xparameters.h" #include <stdio.h> #include <MyDisp.h> #include <mtds.h> #include "sleep.h" #include "xil_exception.h" #include "xttcps.h" #include "xscugic.h" extern "C"{ #include "PmodGPIO.h" #include "xgpiops.h" } #define GPIO_DEVICE_ID XPAR_XGPIOPS_0_DEVICE_ID #define INTC_DEVICE_ID XPAR_SCUGIC_0_DEVICE_ID #define TICK_TIMER_FREQ_HZ 100 #define TTC_TICK_DEVICE_ID XPAR_XTTCPS_0_DEVICE_ID #define TTC_TICK_INTR_ID XPAR_XTTCPS_0_INTR //static PmodGPIO pmoda; static void TickHandler(void *CallBackRef); int SetupTicker(XTtcPs *TtcPsInst,u16 DeviceID,u16 TtcTickIntrID,XScuGic *InterruptController); static int SetupInterruptSystem(u16 IntcDeviceID,XScuGic *IntcInstancePtr); int SetupTimer(u16 DeviceID,XTtcPs *TtcPsInst); void set_pwm(u32 cycle); typedef struct { u32 OutputHz; /* Output frequency */ XInterval Interval; /* Interval value */ u8 Prescaler; /* Prescaler value */ u16 Options; /* Option settings */ } TmrCntrSetup; XGpioPs Gpio; XGpioPs_Config *ConfigPtr; XTtcPs_Config *TtcConfig; XTtcPs ttcTimer; TmrCntrSetup *TimerSetup; XScuGic InterruptController; /* Interrupt controller instance */ XTtcPs TtcPsInst; u32 MatchValue; static TmrCntrSetup SettingsTable={TICK_TIMER_FREQ_HZ, 0, 0, 0}; int main() { char *btnUp = (char*) "Images/IMG_0001.BMP"; char *btnDown = (char*) "Images/IMG_0002.BMP"; TmrCntrSetup SettingsTable= {TICK_TIMER_FREQ_HZ, 0, 0, 0}; ConfigPtr = XGpioPs_LookupConfig(GPIO_DEVICE_ID); XGpioPs_CfgInitialize(&Gpio, ConfigPtr,ConfigPtr->BaseAddr); XGpioPs_SetDirectionPin(&Gpio, 54, 1); XGpioPs_SetOutputEnablePin(&Gpio, 54, 1); printf("www.adiuvoengineering.com

\r"); printf("DC Motor Control Example

\r"); mydisp.begin(); mydisp.clearDisplay(clrBlack); mydisp.setForeground(clrWhite); mydisp.setPen(penSolid); mydisp.setForeground(clrBlue); mydisp.drawImage((char*) "Images/ADIUVO.BMP", 20, 20); mydisp.drawText((char*) "www.adiuvoengineering.com", 20, 80); //mydisp.drawText((char*) "DC Motor Control", 10, 250); mydisp.drawText((char*) "Forward", 10, 140); mydisp.drawText((char*) "Reverse", 100, 140); mydisp.drawText((char*) "Stop", 190, 140); mydisp.drawText((char*) "25%", 10, 200); mydisp.drawText((char*) "33%", 100, 200); mydisp.drawText((char*) "50%", 190, 200); mydisp.drawText((char*) "66%", 10, 260); mydisp.drawText((char*) "75%", 100, 260); mydisp.drawText((char*) "100%", 190, 260); mydisp.createButton(0, btnUp, btnDown, 10, 150); mydisp.createButton(1, btnUp, btnDown, 100, 150); mydisp.createButton(2, btnUp, btnDown, 190, 150); mydisp.createButton(3, btnUp, btnDown, 10, 210); mydisp.createButton(4, btnUp, btnDown, 100, 210); mydisp.createButton(5, btnUp, btnDown, 190, 210); mydisp.createButton(6, btnUp, btnDown, 10, 270); mydisp.createButton(7, btnUp, btnDown, 100, 270); mydisp.createButton(8, btnUp, btnDown, 190, 270); mydisp.enableButton(0, true); mydisp.enableButton(1, true); mydisp.enableButton(2, true); mydisp.enableButton(3, true); mydisp.enableButton(4, true); mydisp.enableButton(5, true); mydisp.enableButton(6, true); mydisp.enableButton(7, true); mydisp.enableButton(8, true); mydisp.drawButton(0, mydisp.isTouched(0) ? BUTTON_DOWN : BUTTON_UP); mydisp.drawButton(1, mydisp.isTouched(1) ? BUTTON_DOWN : BUTTON_UP); mydisp.drawButton(2, mydisp.isTouched(2) ? BUTTON_DOWN : BUTTON_UP); mydisp.drawButton(3, mydisp.isTouched(0) ? BUTTON_DOWN : BUTTON_UP); mydisp.drawButton(4, mydisp.isTouched(1) ? BUTTON_DOWN : BUTTON_UP); mydisp.drawButton(5, mydisp.isTouched(2) ? BUTTON_DOWN : BUTTON_UP); mydisp.drawButton(6, mydisp.isTouched(0) ? BUTTON_DOWN : BUTTON_UP); mydisp.drawButton(7, mydisp.isTouched(1) ? BUTTON_DOWN : BUTTON_UP); mydisp.drawButton(8, mydisp.isTouched(2) ? BUTTON_DOWN : BUTTON_UP); //GPIO_begin(&pmoda,XPAR_PMODGPIO_0_AXI_LITE_GPIO_BASEADDR,0x00); SetupInterruptSystem(INTC_DEVICE_ID, &InterruptController); SetupTicker(&ttcTimer,TTC_TICK_DEVICE_ID,TTC_TICK_INTR_ID,&InterruptController); u8 DutyCycle; mydisp.drawText((char*) "Forward", 20, 100); while(1){ mydisp.checkTouch(); switch (mydisp.getButton()) { case 0: // forward mydisp.drawText((char*) "Forward", 20, 100); set_pwm(0); usleep(1000000); XGpioPs_WritePin(&Gpio, 54, 0x0); set_pwm(DutyCycle); break; case 1: //reverse mydisp.drawText((char*) "Reverse", 20, 100); set_pwm(0); usleep(1000000); XGpioPs_WritePin(&Gpio, 54, 0x1); set_pwm(DutyCycle); break; case 2: //stop mydisp.drawText((char*) "Stop ", 20, 100); mydisp.drawText((char*) " ", 100, 100); set_pwm(0); break; case 3: //25% mydisp.drawText((char*) "25%", 100, 100); DutyCycle = 25; set_pwm(DutyCycle); break; case 4: //33% mydisp.drawText((char*) "33%", 100, 100); DutyCycle = 33; set_pwm(DutyCycle); break; case 5: //50% mydisp.drawText((char*) "50%", 100, 100); DutyCycle = 50; set_pwm(DutyCycle); break; case 6: //66% mydisp.drawText((char*) "66%", 100, 100); DutyCycle = 66; set_pwm(DutyCycle); break; case 7: //75% mydisp.drawText((char*) "75%", 100, 100); DutyCycle = 75; set_pwm(DutyCycle); break; case 8: //100% mydisp.drawText((char*) "100%", 100, 100); DutyCycle = 100; set_pwm(DutyCycle); break; } } return 0; }

Initial Testing

Once this code was all up and running on the board, before I could connect it to the motor I wanted to double check the PWM was correct and the directions signal change was safe.

If we change the direction at the same time as we are applying PWM signal, then there is potential for a short circuit and damage to be caused.

To test this I connected a oscilloscope to the Pmod outputs and ensured the PWM signal was as expected.

50:50 PWM

Testing of the Demo

Once I was happy with the PWM output and the safe changing of directions, I connected a motor to the board and tested out all of the different speeds. In the video below you can see the speed changing (and the drive current) as the different speeds are set on the touch screen.

We also see the direction changing and the motor being stopped.

Testing of the Demo

Conclusion

This project has shown how easy it is to set up and get the MiniZed controlling one or several motors.

The MiniZed might be mini but it is mighty

See previous projects here.

Additional Information on Xilinx FPGA / SoC Development can be found weekly on MicroZed Chronicles.