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:
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:
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 ClockbuffersClockingWizard_Clock_t
– Beinhaltet die Konfiguration eines einzelnen Signalausgangs des Clocking Wizards
Das Objekt ClockingWizard_Clock_t
beinhaltet die folgenden Felder:
Für die Ausgangsfrequenz eines jeden Kanals gilt:
Ü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.
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
Hi Martin,
anscheinend ist das Archiv nicht mit im Repo gelandet…
Du kannst ihn dir das Archiv mit dem Code über meine Nextcloud runterladen:
https://cloud.server-kampert.de/s/xxwdH3yDSniPXga
Gruß
Daniel