Kampis Elektroecke

Clocking Wizard

Mit Hilfe des Clocking Wizard lassen sich beinahe beliebige Taktfrequenzen für unterschiedliche Bereiche der programmierbaren Logik generieren. Als Zusatz bringt der Clocking Wizard ein AXI4Lite Interface mit, über das die Frequenz, die Phase und das Tastverhältnis eines jeden Ausgangssignals zur Laufzeit konfiguriert werden kann. In diesem Teil des Tutorials möchte ich zeigen, wie der Clocking Wizard funktioniert und wie ein Treiber geschrieben werden kann um das Ausgangssignal zur Laufzeit zu verändern.

Der Clocking Wizard kann über den IP Integrator von Vivado genutzt und konfiguriert werden.

Bei den ZYNQ SoC können für die Takterzeugung zwei verschiedene Schaltungsarchitekturen genutzt werden, wobei ich in diesem Beispiel den MMCM verwenden werde:

Schaltungsarchitektur Beschreibung
PLL
Phased Locked Loop

Ersetzt den älteren DCM (Digital Clock Manager). Eine PLL kann mehrere verschiedene Frequenzen gleichzeitig erzeugen und weißt ein besseres Jitter Verhalten und eine höhere Genauigkeit als der DCM auf.

Die PLL kann nicht dynamisch die Phasenlage eines Signals verändern.

MMCM
Mixed Mode Clock Manager

Eine PLL mit Komponenten des älteren DCM um auch die Phasenlage des Signals verändern zu können. Hierbei handelt es sich um sowohl eine analoge (Frequenz), als auch um eine digitale (Phasenlage) Schaltung.

Ein MMCM benötigt zudem mehr Chipfläche als eine PLL.

Eine genaue Auflistung über die zur Verfügung stehenden Taktquellen liefert das Datenblatt des Chips.

Insgesamt können auf dem ZYBO also bis zu 2 MMCM und 2 PLLs genutzt werden.

Damit der Clocking Wizard zur Laufzeit konfiguriert werden kann, muss die Option Dynamic Reconfig unter Clocking Features aktiviert werden, wodurch der Clocking Wizard mit einem AXI4Lite oder einem DRP Interface versehen wird. Es wird das AXI4Lite Interface ausgewählt und abschließend wird noch die Frequenz des Eingangtaktes eingetragen.

Der Clocking Wizard wird dann noch um ein Processing System und einen AXI GPIO Block erweitert.

In der Software wird als erstes der GPIO-Block initialisiert und dann der Clocking Wizard initialisiert:

ConfigPtr = XGpio_LookupConfig(XPAR_GPIO_0_DEVICE_ID);
if(ConfigPtr == NULL)
{
	xil_printf("[ERROR] Invalid GPIO switch configuration!\n\r");
	return XST_FAILURE;
}

if(XGpio_CfgInitialize(&Switches, ConfigPtr, ConfigPtr->BaseAddress) != XST_SUCCESS)
{
	xil_printf("[ERROR] Can not initialize switches!\n\r");
	return XST_FAILURE;
}

if(ClockingWizard_Init(&Device, XPAR_CLOCKINGWIZARD_BASEADDR) != XST_SUCCESS)
{
	xil_printf("[ERROR] Can not initialize Clocking Wizard!\n\r");
	return XST_FAILURE;
}

Der Clocking Wizard wird in der Software über ein Objekt namens Device vom Typ ClockingWizard_t dargestellt. Dieses Objekt ist folgendermaßen aufgebaut:

Feld Beschreibung
BaseAddr Adresse des jeweiligen Clocking Wizard
isLocked Signalisiert ob das Ausgangssignal stabil ist
DIVCLK_DIVIDE Parameter DIVCLK_DIVIDE des Clocking Wizard
CLKFBOUT_MULT Vorkommas des Parameters CLKFBOUT des Clocking Wizard
CLKFBOUT_Frac_Multiply Nachkommas des Parameters CLKFBOUT des Clocking Wizard
CLKFBOUT_PHASE Parameter CLKFBOUT_PHASE des Clocking Wizard

Die Funktion ClockingWizard_Init initialisiert den Clocking Wizard und lädt die Instanz Device mit den entsprechenden Werten.

u32 ClockingWizard_Init(ClockingWizard_t* Device, const UINTPTR EffectiveAddr)
{
	if((EffectiveAddr == 0x00) || (Device == NULL))
	{
		return XST_INVALID_PARAM;
	}

	Device->BaseAddr = EffectiveAddr;

	return ClockingWizard_Reset(Device) | ClockingWizard_GetClockBuffer(Device);
}

Während der Initialisierung wird der Clocking Wizard zurückgesetzt, indem das Bitmuster 0x0A in das SR-Register geschrieben wird.

u32 ClockingWizard_Reset(ClockingWizard_t* Device)
{
	if(Device == NULL)
	{
		return XST_INVALID_PARAM;
	}

	Xil_Out32(Device->BaseAddr + 0x000, 0x0A);
	return ClockingWizard_WaitForLock(Device, 10000);
}

Nach einem Reset muss gewartet werden, bis das Ausgangssignal stabil ist.

static u32 ClockingWizard_WaitForLock(ClockingWizard_t* Device, const u32 Timeout)
{
        u32 Timeout_Temp = Timeout;
	while(!(Xil_In32(Device->BaseAddr + 0x004) & (0x01 << 0x00)))
	{

		if(Timeout_Temp-- == 0x00)
		{
			return XST_TIMEOUT;
		}
	}

	Device->isLocked = Xil_In32(Device->BaseAddr + 0x004) & (0x01 << 0x00);

	return XST_SUCCESS;
}

Sobald der Reset erfolgreich durchgeführt worden ist, werden die Register für den Clockbuffer ausgelesen und die Initialisierung abgeschlossen.

u32 ClockingWizard_GetClockBuffer(ClockingWizard_t* Device)
{
	if(Device == NULL)
	{
		return XST_INVALID_PARAM;
	}

	u32 ClockConfiguration = Xil_In32(Device->BaseAddr + 0x200);
	Device->DIVCLK_DIVIDE = ClockConfiguration & 0xFF;
	Device->CLKFBOUT_MULT = (ClockConfiguration >> 0x08) & 0xFF;
	Device->CLKFBOUT_Frac_Multiply = (ClockConfiguration >> 0x10) & 0xFF;
	Device->CLKFBOUT_PHASE = Xil_In32(Device->BaseAddr + 0x204);

	return XST_SUCCESS;
}

Der Treiber verwendet zwei verschiedene Objekte um den Clocking Wizard zu konfigurieren:

  • ClockingWizard_t – Beinhaltet die allgemeine Konfiguration des Clocking Wizards und des Clockbuffers
  • ClockingWizard_Clock_t – Beinhaltet die Konfiguration eines einzelnen Signalausgangs des Clocking Wizards

Das Objekt ClockingWizard_Clock_t beinhaltet die folgenden Felder:

Feld Beschreibung
Channel Ausgangskanal
DIVIDE Vorkommastelle des Parameters DIVIDE des Ausgangskanals
FRAC_Divide Nachkommastelle des Parameters DIVIDE des Ausgangskanals
PHASE Parameter PHASE des Ausgangskanals
DUTY Parameter DUTY des Ausgangskanals

Für die Ausgangsfrequenz eines jeden Kanals gilt:

f_{Out} =f_{In}\cdot \frac{CLKFBOUT\_MULT\_F}{DIVCLK\_DIVIDE}\cdot \frac{1}{DIVIDE}

Über die Funktionen ClockingWizard_SetClockBuffer und ClockingWizard_SetOutput können der Clockbuffer und der jeweilige Ausgangskanal modifiziert werden um so die Ausgangsfrequenz und die Phasenlage anzupassen.

u32 ClockingWizard_SetClockBuffer(ClockingWizard_t* Device)
{
	if(Device == NULL)
	{
		return XST_INVALID_PARAM;
	}

	u32 ClockConfiguration = (Device->CLKFBOUT_Frac_Multiply << 0x10)  | (Device->CLKFBOUT_MULT << 0x08) | Device->DIVCLK_DIVIDE;
	Xil_Out32(Device->BaseAddr + 0x200, ClockConfiguration);
	Xil_Out32(Device->BaseAddr + 0x204, Device->CLKFBOUT_PHASE);

	return ClockingWizard_Update(Device);
}

u32 ClockingWizard_SetOutput(ClockingWizard_t* Device, ClockingWizard_Clock_t* Output)
{
	u32 BaseAddressOffset = 0x00;

	if((Device == NULL) || (Output == NULL) || (Output->Channel > 0x06) || (Output->Channel < 0x00))
	{
		return XST_INVALID_PARAM;
	}

	BaseAddressOffset = 0x208 + 0x0C * Output->Channel;

	Xil_Out32(Device->BaseAddr + BaseAddressOffset, (((u32)Output->FRAC_Divide << 0x08)) | Output->DIVIDE);
	Xil_Out32(Device->BaseAddr + BaseAddressOffset + 0x04, Output->PHASE);
	Xil_Out32(Device->BaseAddr + BaseAddressOffset + 0x08, Output->DUTY);

	return ClockingWizard_Update(Device);
}

Alternativ kann die Funktion ClockingWizard_LoadDefault genutzt werden um die Einstellungen aus dem IP Integrator zu laden.

u32 ClockingWizard_LoadDefault(ClockingWizard_t* Device)
{
	if(Device == NULL)
	{
		return XST_INVALID_PARAM;
	}

	Xil_Out32(Device->BaseAddr + 0x25C, (0x01 << 0x00));
	return ClockingWizard_WaitForLock(Device, 10000);
}

Damit ist der Treiber abgeschlossen und bereit für den Einsatz. In dem Beispielprojekt können über die Schalter des ZYBO vier verschiedene Ausgangsfrequenzen eingestellt werden:

#include "xgpio.h"
#include "ClockingWizard/ClockingWizard.h"

ClockingWizard_t Device;
ClockingWizard_Clock_t OutputClock;

XGpio_Config* GPIO_ConfigPtr;
XGpio GPIO;

XClk_Wiz_Config* ClkWiz_ConfigPtr;

u32 Input = 0x00;
u32 InputOld = 0x00;

int main(void)
{
	xil_printf("[INFO] Clocking Wizard example\n\r");

	GPIO_ConfigPtr = XGpio_LookupConfig(XPAR_GPIO_0_DEVICE_ID);
	if(GPIO_ConfigPtr == NULL)
	{
		xil_printf("[ERROR] Invalid GPIO switch configuration!\n\r");
		return XST_FAILURE;
	}

	if(XGpio_CfgInitialize(&GPIO, GPIO_ConfigPtr, GPIO_ConfigPtr->BaseAddress) != XST_SUCCESS)
	{
		xil_printf("[ERROR] Can not initialize switches!\n\r");
		return XST_FAILURE;
	}

	if(ClockingWizard_Init(&Device, XPAR_CLOCKINGWIZARD_BASEADDR) != XST_SUCCESS)
	{
		xil_printf("[ERROR] Can not initialize Clocking Wizard!\n\r");
		return XST_FAILURE;
	}

	ClockingWizard_GetOutput(&Device, &OutputClock);

	xil_printf("[INFO] MMC configuration:\n\r");
	xil_printf("	DIVCLK Divide: %lu\n\r", Device.DIVCLK_DIVIDE);
	xil_printf("	CLKBOUT Mult: %lu\n\r", Device.CLKFBOUT_MULT);
	xil_printf("	CLKBOUT Frac Mult: %lu\n\r", Device.CLKFBOUT_Frac_Multiply);
	xil_printf("	CLKBOUT Duty: %lu\n\r", Device.CLKFBOUT_PHASE);
	xil_printf("[INFO] Channel configuration:\n\r");
	xil_printf("	Divide: %lu\n\r", OutputClock.DIVIDE);
	xil_printf("	Frac Divide: %lu\n\r", OutputClock.FRAC_Divide);
	xil_printf("	Phase: %lu\n\r", OutputClock.PHASE);
	xil_printf("	Duty: %lu\n\r", OutputClock.DUTY);

	while(1)
	{
		InputOld = Input;

		while(InputOld == Input)
		{
			Input = XGpio_DiscreteRead(&GPIO, 1);
		}

		xil_printf("[INFO] Input selection: %lu\n\r", Input);

		switch(Input)
		{
			case(0):
			{
				xil_printf("[INFO] Set default clock...\n\r");
				ClockingWizard_LoadDefault(&Device);
				break;
			}
			case(1):
			{
				xil_printf("[INFO] Set output clock to 40 MHz...\n\r");
				Device.DIVCLK_DIVIDE = 1;
				Device.CLKFBOUT_MULT = 8;
				Device.CLKFBOUT_Frac_Multiply = 0;
				OutputClock.DIVIDE = 25;
				OutputClock.FRAC_Divide = 0;
				ClockingWizard_SetClockBuffer(&Device);
				ClockingWizard_SetOutput(&Device, &OutputClock);
				break;
			}
			case(2):
			{
				xil_printf("[INFO] Set output clock to 65 MHz...\n\r");
				Device.DIVCLK_DIVIDE = 1;
				Device.CLKFBOUT_MULT = 8;
				Device.CLKFBOUT_Frac_Multiply = 125;
				OutputClock.DIVIDE = 15;
				OutputClock.FRAC_Divide = 625;
				ClockingWizard_SetClockBuffer(&Device);
				ClockingWizard_SetOutput(&Device, &OutputClock);
				break;
			}
			case(4):
			{
				xil_printf("[INFO] Set output clock to 108 MHz...\n\r");
				Device.DIVCLK_DIVIDE = 5;
				Device.CLKFBOUT_MULT = 40;
				Device.CLKFBOUT_Frac_Multiply = 5;
				OutputClock.DIVIDE = 9;
				OutputClock.FRAC_Divide = 375;
				ClockingWizard_SetClockBuffer(&Device);
				ClockingWizard_SetOutput(&Device, &OutputClock);
				break;
			}
			default:
			{
				xil_printf("[ERROR] Invalid selection!\n\r");
				break;
			}
		}

		for(u32 i = 0x00; i < 0xFFFFFFF; i++);
	}

	return XST_SUCCESS;
}

Das komplette Projekt steht in meinem GitHub-Repository zum Download bereit.

2 Kommentare

  1. Hallo Daniel,

    danke für den Artikel. Ich bin auf der Suche nach einem brauchbaren Xilinx Treiber für den Clock Wizard gestoßen. Da bedienst du eine große Xilinx-typische Lücke ;)

    In deinem Gitlab konnte ich den C-Code nicht finden. Kannst du mir sagen, wo der liegt?

    Danke und viele Grüße aus Bielefeld

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert