Commit 04a8f280 authored by Federico Vaga's avatar Federico Vaga

kernel: add FPGA programming logic

Signed-off-by: Federico Vaga's avatarFederico Vaga <federico.vaga@vaga.pv.it>
parent 0f61b0e2
......@@ -13,3 +13,4 @@ ccflags-y += -DGIT_VERSION=\"$(GIT_VERSION)\"
obj-m := spec.o
spec-objs := spec-core.o
spec-objs += loader-ll.o
/*
* This is the low-level engine of firmware loading. It is meant
* to be compiled both as kernel code and user code, using the associated
* header to differentiate
*/
#define __LOADER_LL_C__ /* Callers won't define this symbol */
#ifdef __KERNEL__
#include "spec.h"
#include "loader-ll.h"
#else
#include "loader-userspace.h"
#endif
/* These must be set to choose the FPGA configuration mode */
#define GPIO_BOOTSEL0 15
#define GPIO_BOOTSEL1 14
static inline uint8_t reverse_bits8(uint8_t x)
{
x = ((x >> 1) & 0x55) | ((x & 0x55) << 1);
x = ((x >> 2) & 0x33) | ((x & 0x33) << 2);
x = ((x >> 4) & 0x0f) | ((x & 0x0f) << 4);
return x;
}
static uint32_t unaligned_bitswap_le32(const uint32_t *ptr32)
{
static uint32_t tmp32;
static uint8_t *tmp8 = (uint8_t *) &tmp32;
static uint8_t *ptr8;
ptr8 = (uint8_t *) ptr32;
*(tmp8 + 0) = reverse_bits8(*(ptr8 + 0));
*(tmp8 + 1) = reverse_bits8(*(ptr8 + 1));
*(tmp8 + 2) = reverse_bits8(*(ptr8 + 2));
*(tmp8 + 3) = reverse_bits8(*(ptr8 + 3));
return tmp32;
}
static inline void gpio_out(int fd, void __iomem *bar4, const uint32_t addr, const int bit, const int value)
{
uint32_t reg;
reg = lll_read(fd, bar4, addr);
if(value)
reg |= (1<<bit);
else
reg &= ~(1<<bit);
lll_write(fd, bar4, reg, addr);
}
/*
* Unfortunately, most of the following is from fcl_gn4124.cpp, for which
* the license terms are at best ambiguous.
*/
int loader_low_level(int fd, void __iomem *bar4, const void *data, int size8)
{
int size32 = (size8 + 3) >> 2;
const uint32_t *data32 = data;
int ctrl = 0, i, done = 0, wrote = 0;
/* configure Gennum GPIO to select GN4124->FPGA configuration mode */
gpio_out(fd, bar4, GNGPIO_DIRECTION_MODE, GPIO_BOOTSEL0, 0);
gpio_out(fd, bar4, GNGPIO_DIRECTION_MODE, GPIO_BOOTSEL1, 0);
gpio_out(fd, bar4, GNGPIO_OUTPUT_ENABLE, GPIO_BOOTSEL0, 1);
gpio_out(fd, bar4, GNGPIO_OUTPUT_ENABLE, GPIO_BOOTSEL1, 1);
gpio_out(fd, bar4, GNGPIO_OUTPUT_VALUE, GPIO_BOOTSEL0, 1);
gpio_out(fd, bar4, GNGPIO_OUTPUT_VALUE, GPIO_BOOTSEL1, 0);
lll_write(fd, bar4, 0x00, FCL_CLK_DIV);
lll_write(fd, bar4, 0x40, FCL_CTRL); /* Reset */
i = lll_read(fd, bar4, FCL_CTRL);
if (i != 0x40) {
printk(KERN_ERR "%s: %i: error\n", __func__, __LINE__);
return -EIO;
}
lll_write(fd, bar4, 0x00, FCL_CTRL);
lll_write(fd, bar4, 0x00, FCL_IRQ); /* clear pending irq */
switch(size8 & 3) {
case 3: ctrl = 0x116; break;
case 2: ctrl = 0x126; break;
case 1: ctrl = 0x136; break;
case 0: ctrl = 0x106; break;
}
lll_write(fd, bar4, ctrl, FCL_CTRL);
lll_write(fd, bar4, 0x00, FCL_CLK_DIV); /* again? maybe 1 or 2? */
lll_write(fd, bar4, 0x00, FCL_TIMER_CTRL); /* "disable FCL timr fun" */
lll_write(fd, bar4, 0x10, FCL_TIMER_0); /* "pulse width" */
lll_write(fd, bar4, 0x00, FCL_TIMER_1);
/*
* Set delay before data and clock is applied by FCL
* after SPRI_STATUS is detected being assert.
*/
lll_write(fd, bar4, 0x08, FCL_TIMER2_0); /* "delay before data/clk" */
lll_write(fd, bar4, 0x00, FCL_TIMER2_1);
lll_write(fd, bar4, 0x17, FCL_EN); /* "output enable" */
ctrl |= 0x01; /* "start FSM configuration" */
lll_write(fd, bar4, ctrl, FCL_CTRL);
while(size32 > 0)
{
/* Check to see if FPGA configuation has error */
i = lll_read(fd, bar4, FCL_IRQ);
if ( (i & 8) && wrote) {
done = 1;
printk("%s: %i: done after %i\n", __func__, __LINE__,
wrote);
} else if ( (i & 0x4) && !done) {
printk("%s: %i: error after %i\n", __func__, __LINE__,
wrote);
return -EIO;
}
/* Wait until at least 1/2 of the fifo is empty */
while (lll_read(fd, bar4, FCL_IRQ) & (1<<5))
;
/* Write a few dwords into FIFO at a time. */
for (i = 0; size32 && i < 32; i++) {
lll_write(fd, bar4, unaligned_bitswap_le32(data32),
FCL_FIFO);
data32++; size32--; wrote++;
}
}
lll_write(fd, bar4, 0x186, FCL_CTRL); /* "last data written" */
/* Checking for the "interrupt" condition is left to the caller */
return wrote;
}
void waitdone_low_level(int fd, void __iomem *bar4)
{
while ( (lll_read(fd, bar4, FCL_IRQ) & 0x8) == 0 )
;
}
/* After programming, we fix gpio lines so pci can access the flash */
void gpiofix_low_level(int fd, void __iomem *bar4)
{
gpio_out(fd, bar4, GNGPIO_OUTPUT_VALUE, GPIO_BOOTSEL0, 0);
gpio_out(fd, bar4, GNGPIO_OUTPUT_VALUE, GPIO_BOOTSEL1, 0);
gpio_out(fd, bar4, GNGPIO_OUTPUT_ENABLE, GPIO_BOOTSEL0, 0);
gpio_out(fd, bar4, GNGPIO_OUTPUT_ENABLE, GPIO_BOOTSEL1, 0);
}
void loader_reset_fpga(int fd, void __iomem *bar4)
{
uint32_t reg;
/* After reprogramming, reset the FPGA using the gennum register */
reg = lll_read(fd, bar4, GNPCI_SYS_CFG_SYSTEM);
/*
* This _fucking_ register must be written with extreme care,
* becase some fields are "protected" and some are not. *hate*
*/
lll_write(fd, bar4, (reg & ~0xffff) | 0x3fff, GNPCI_SYS_CFG_SYSTEM);
lll_write(fd, bar4, (reg & ~0xffff) | 0x7fff, GNPCI_SYS_CFG_SYSTEM);
}
/*
* This header differentiates between kernel-mode and user-mode compilation,
* as loader-ll.c is meant to be used in both contexts.
*/
#ifndef __iomem
#define __iomem /* nothing, for user space */
#endif
/* "fd" was relevant in user space, but now it is ignored */
extern int loader_low_level(int fd, void __iomem *bar4, const void *, int);
extern void waitdone_low_level(int fd, void __iomem *bar4);
extern void gpiofix_low_level(int fd, void __iomem *bar4);
extern void loader_reset_fpga(int fd, void __iomem *bar4);
/* The following part implements a different access rule for user and kernel */
#ifdef __LOADER_LL_C__
#ifdef __KERNEL__
#include <asm/io.h>
//#include <linux/kernel.h> /* for printk */
static inline void lll_write(int fd, void __iomem *bar4, u32 val, int reg)
{
writel(val, bar4 + reg);
}
static inline u32 lll_read(int fd, void __iomem *bar4, int reg)
{
return readl(bar4 + reg);
}
#else /* ! __KERNEL__ */
#include <stdio.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <errno.h>
static inline void lll_write(int fd, void __iomem *bar4, uint32_t val, int reg)
{
struct rr_iocmd iocmd = {
.datasize = 4,
.address = reg | __RR_SET_BAR(4),
};
iocmd.data32 = val;
if (ioctl(fd, RR_WRITE, &iocmd) < 0) perror("ioctl");
return;
}
static inline uint32_t lll_read(int fd, void __iomem *bar4, int reg)
{
struct rr_iocmd iocmd = {
.datasize = 4,
.address = reg | __RR_SET_BAR(4),
};
if (ioctl(fd, RR_READ, &iocmd) < 0) perror("ioctl");
return iocmd.data32;
}
#define KERN_ERR /* nothing */
#define printk(format, ...) fprintf (stderr, format, ## __VA_ARGS__)
#endif
#endif /* __LOADER_LL_C__ */
......@@ -9,82 +9,19 @@
#include <linux/bitmap.h>
#include <linux/cdev.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/ioport.h>
#include <linux/jiffies.h>
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>
#include <linux/vmalloc.h>
#define PCI_VENDOR_ID_CERN (0x10DC)
#define PCI_DEVICE_ID_SPEC_45T (0x018D)
#define PCI_DEVICE_ID_SPEC_100T (0x01A2)
#define PCI_VENDOR_ID_GENNUM (0x1A39)
#define PCI_DEVICE_ID_GN4124 (0x0004)
#define SPEC_MINOR_MAX (64)
#define SPEC_FLAG_BITS (8)
/* Registers for GN4124 access */
enum {
/* page 106 */
GNPPCI_MSI_CONTROL = 0x48, /* actually, 3 smaller regs */
GNPPCI_MSI_ADDRESS_LOW = 0x4c,
GNPPCI_MSI_ADDRESS_HIGH = 0x50,
GNPPCI_MSI_DATA = 0x54,
GNPCI_SYS_CFG_SYSTEM = 0x800,
/* page 130 ff */
GNINT_CTRL = 0x810,
GNINT_STAT = 0x814,
GNINT_CFG_0 = 0x820,
GNINT_CFG_1 = 0x824,
GNINT_CFG_2 = 0x828,
GNINT_CFG_3 = 0x82c,
GNINT_CFG_4 = 0x830,
GNINT_CFG_5 = 0x834,
GNINT_CFG_6 = 0x838,
GNINT_CFG_7 = 0x83c,
#define GNINT_CFG(x) (GNINT_CFG_0 + 4 * (x))
/* page 146 ff */
GNGPIO_BASE = 0xA00,
GNGPIO_BYPASS_MODE = GNGPIO_BASE,
GNGPIO_DIRECTION_MODE = GNGPIO_BASE + 0x04, /* 0 == output */
GNGPIO_OUTPUT_ENABLE = GNGPIO_BASE + 0x08,
GNGPIO_OUTPUT_VALUE = GNGPIO_BASE + 0x0C,
GNGPIO_INPUT_VALUE = GNGPIO_BASE + 0x10,
GNGPIO_INT_MASK = GNGPIO_BASE + 0x14, /* 1 == disabled */
GNGPIO_INT_MASK_CLR = GNGPIO_BASE + 0x18, /* irq enable */
GNGPIO_INT_MASK_SET = GNGPIO_BASE + 0x1C, /* irq disable */
GNGPIO_INT_STATUS = GNGPIO_BASE + 0x20,
GNGPIO_INT_TYPE = GNGPIO_BASE + 0x24, /* 1 == level */
GNGPIO_INT_VALUE = GNGPIO_BASE + 0x28, /* 1 == high/rise */
GNGPIO_INT_ON_ANY = GNGPIO_BASE + 0x2C, /* both edges */
/* page 158 ff */
FCL_BASE = 0xB00,
FCL_CTRL = FCL_BASE,
FCL_STATUS = FCL_BASE + 0x04,
FCL_IODATA_IN = FCL_BASE + 0x08,
FCL_IODATA_OUT = FCL_BASE + 0x0C,
FCL_EN = FCL_BASE + 0x10,
FCL_TIMER_0 = FCL_BASE + 0x14,
FCL_TIMER_1 = FCL_BASE + 0x18,
FCL_CLK_DIV = FCL_BASE + 0x1C,
FCL_IRQ = FCL_BASE + 0x20,
FCL_TIMER_CTRL = FCL_BASE + 0x24,
FCL_IM = FCL_BASE + 0x28,
FCL_TIMER2_0 = FCL_BASE + 0x2C,
FCL_TIMER2_1 = FCL_BASE + 0x30,
FCL_DBG_STS = FCL_BASE + 0x34,
FCL_FIFO = 0xE00,
PCI_SYS_CFG_SYSTEM = 0x800
};
#include "spec.h"
#include "loader-ll.h"
static DECLARE_BITMAP(spec_minors, SPEC_MINOR_MAX);
......@@ -92,23 +29,6 @@ static dev_t basedev;
static struct class *spec_class;
/**
* struct spec_dev - SPEC instance
* It describes a SPEC device instance.
* @cdev Char device descriptor
* @dev Linux device instance descriptor
* @flags collection of bit flags
* @remap ioremap of PCI bar 0, 2, 4
*/
struct spec_dev {
struct cdev cdev;
struct device dev;
DECLARE_BITMAP(flags, SPEC_FLAG_BITS);
void __iomem *remap[3]; /* ioremap of bar 0, 2, 4 */
};
/**
* It reads a 32bit register from the gennum chip
* @spec spec device instance
......@@ -192,6 +112,43 @@ static inline void spec_minor_put(unsigned int minor)
}
/* Load the FPGA. This bases on loader-ll.c, a kernel/user space thing */
static int spec_load_fpga(struct spec_dev *spec, const void *data, int size)
{
int i, wrote;
unsigned long j;
/* loader_low_level is designed to run from user space too */
wrote = loader_low_level(0 /* unused fd */,
spec->remap[2], data, size);
j = jiffies + 2 * HZ;
/* Wait for DONE interrupt */
while (1) {
udelay(100);
i = readl(spec->remap[2] + FCL_IRQ);
if (i & 0x8)
break;
if (i & 0x4) {
dev_err(&spec->dev,
"FPGA program error after %i writes\n",
wrote);
return -ETIMEDOUT;
}
if (time_after(jiffies, j)) {
dev_err(&spec->dev,
"FPGA timeout after %i writes\n", wrote);
return -ETIMEDOUT;
}
}
gpiofix_low_level(0 /* unused fd */, spec->remap[2]);
loader_reset_fpga(0 /* unused fd */, spec->remap[2]);
return 0;
}
/**
* It prepares the FPGA to receive a new bitstream.
* @inode file system node
......@@ -209,13 +166,30 @@ static int spec_open(struct inode *inode, struct file *file)
cdev);
int err;
if (!test_bit(SPEC_FLAG_UNLOCK, spec->flags)) {
dev_info(&spec->dev, "Application FPGA programming blocked\n");
return -EPERM;
}
err = try_module_get(file->f_op->owner);
if (err == 0)
return -EBUSY;
spec->size = 1024 * 1024 * 4; /* 4MiB to begin with */
spec->buf = vmalloc(spec->size);
if (!spec->buf) {
err = -ENOMEM;
goto err_vmalloc;
}
file->private_data = spec;
return 0;
err_vmalloc:
module_put(file->f_op->owner);
return err;
}
......@@ -230,6 +204,15 @@ static int spec_close(struct inode *inode, struct file *file)
file->private_data = NULL;
spec_load_fpga(spec, spec->buf, spec->size);
vfree(spec->buf);
spec->size = 0;
spin_lock(&spec->lock);
clear_bit(SPEC_FLAG_UNLOCK, spec->flags);
spin_unlock(&spec->lock);
module_put(file->f_op->owner);
return 0;
......@@ -248,7 +231,33 @@ static int spec_close(struct inode *inode, struct file *file)
static ssize_t spec_write(struct file *file, const char __user *buf,
size_t count, loff_t *offp)
{
return -EINVAL;
struct spec_dev *spec = file->private_data;
int err;
if (unlikely(!spec->buf)) {
dev_err(&spec->dev, "No memory left to copy the bitstream\n");
return -ENOMEM;
}
if (unlikely(count + *offp > 1024 * 1024 * 20)) {
dev_err(&spec->dev, "An FPGA bitstream of 20MiB is too big\n");
return -EINVAL;
}
if (count + *offp > spec->size) {
vfree(spec->buf);
spec->size *= 2;
spec->buf = vmalloc(spec->size);
return 0;
}
err = copy_from_user(spec->buf + *offp, buf, count);
if (err)
return err;
*offp += count;
return count;
}
......@@ -263,6 +272,65 @@ static const struct file_operations spec_fops = {
};
/**
* It shows the current AFPGA programming locking status
*/
static ssize_t spec_afpga_lock_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct spec_dev *spec = to_spec_dev(dev);
return snprintf(buf, PAGE_SIZE, "%s\n",
test_bit(SPEC_FLAG_UNLOCK, spec->flags) ?
"unlocked" : "locked");
}
/**
* It unlocks the AFPGA programming when the user write "unlock" or "lock"
*/
static ssize_t spec_afpga_lock_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct spec_dev *spec = to_spec_dev(dev);
unsigned int lock;
if (strncmp(buf, "unlock" , min(6, (int)count)) != 0 &&
strncmp(buf, "lock" , min(4, (int)count)) != 0)
return -EINVAL;
lock = (strncmp(buf, "lock" , min(4, (int)count)) == 0);
spin_lock(&spec->lock);
if (lock)
clear_bit(SPEC_FLAG_UNLOCK, spec->flags);
else
set_bit(SPEC_FLAG_UNLOCK, spec->flags);
spin_unlock(&spec->lock);
return count;
}
static DEVICE_ATTR(lock, 0644, spec_afpga_lock_show, spec_afpga_lock_store);
static struct attribute *spec_dev_attrs[] = {
&dev_attr_lock.attr,
NULL,
};
static const struct attribute_group spec_dev_group = {
.name = "AFPGA",
.attrs = spec_dev_attrs,
};
static const struct attribute_group *spec_dev_groups[] = {
&spec_dev_group,
NULL,
};
/**
* It releases device resources (`device->release()`)
* @dev Linux device instance
......@@ -303,15 +371,15 @@ static int spec_probe(struct pci_dev *pdev,
err = -ENOMEM;
goto err_alloc;
}
dev_set_name(&spec->dev, "spec.%04x:%02x:%02x.%d",
pci_domain_nr(pdev->bus),
pdev->bus->number, PCI_SLOT(pdev->devfn),
PCI_FUNC(pdev->devfn));
dev_set_name(&spec->dev, "spec.%04x",
pdev->bus->number << 8 | PCI_SLOT(pdev->devfn));
spec->dev.class = spec_class;
spec->dev.devt = basedev + minor;
spec->dev.parent = &pdev->dev;
spec->dev.release = spec_release;
spec->dev.groups = spec_dev_groups;
dev_set_drvdata(&spec->dev, spec);
spin_lock_init(&spec->lock);
/* Remap our 3 bars */
for (i = err = 0; i < 3; i++) {
......
/*
* Copyright (C) 2010-2012 CERN (www.cern.ch)
* Author: Alessandro Rubini <rubini@gnudd.com>
*
* Released according to the GNU GPL, version 2 or any later version.
*
* This work is part of the White Rabbit project, a research effort led
* by CERN, the European Institute for Nuclear Research.
*/
#ifndef __SPEC_H__
#define __SPEC_H__
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/pci.h>
#include <linux/spinlock.h>
#define PCI_VENDOR_ID_CERN (0x10DC)
#define PCI_DEVICE_ID_SPEC_45T (0x018D)
#define PCI_DEVICE_ID_SPEC_100T (0x01A2)
#define PCI_VENDOR_ID_GENNUM (0x1A39)
#define PCI_DEVICE_ID_GN4124 (0x0004)
#define SPEC_MINOR_MAX (64)
#define SPEC_FLAG_BITS (8)
#define SPEC_FLAG_UNLOCK BIT(0)
/* Registers for GN4124 access */
enum {
/* page 106 */
GNPPCI_MSI_CONTROL = 0x48, /* actually, 3 smaller regs */
GNPPCI_MSI_ADDRESS_LOW = 0x4c,
GNPPCI_MSI_ADDRESS_HIGH = 0x50,
GNPPCI_MSI_DATA = 0x54,
GNPCI_SYS_CFG_SYSTEM = 0x800,
/* page 130 ff */
GNINT_CTRL = 0x810,
GNINT_STAT = 0x814,
GNINT_CFG_0 = 0x820,
GNINT_CFG_1 = 0x824,
GNINT_CFG_2 = 0x828,
GNINT_CFG_3 = 0x82c,
GNINT_CFG_4 = 0x830,
GNINT_CFG_5 = 0x834,
GNINT_CFG_6 = 0x838,
GNINT_CFG_7 = 0x83c,
#define GNINT_CFG(x) (GNINT_CFG_0 + 4 * (x))
/* page 146 ff */
GNGPIO_BASE = 0xA00,
GNGPIO_BYPASS_MODE = GNGPIO_BASE,
GNGPIO_DIRECTION_MODE = GNGPIO_BASE + 0x04, /* 0 == output */
GNGPIO_OUTPUT_ENABLE = GNGPIO_BASE + 0x08,
GNGPIO_OUTPUT_VALUE = GNGPIO_BASE + 0x0C,
GNGPIO_INPUT_VALUE = GNGPIO_BASE + 0x10,
GNGPIO_INT_MASK = GNGPIO_BASE + 0x14, /* 1 == disabled */
GNGPIO_INT_MASK_CLR = GNGPIO_BASE + 0x18, /* irq enable */
GNGPIO_INT_MASK_SET = GNGPIO_BASE + 0x1C, /* irq disable */
GNGPIO_INT_STATUS = GNGPIO_BASE + 0x20,
GNGPIO_INT_TYPE = GNGPIO_BASE + 0x24, /* 1 == level */
GNGPIO_INT_VALUE = GNGPIO_BASE + 0x28, /* 1 == high/rise */
GNGPIO_INT_ON_ANY = GNGPIO_BASE + 0x2C, /* both edges */
/* page 158 ff */
FCL_BASE = 0xB00,
FCL_CTRL = FCL_BASE,
FCL_STATUS = FCL_BASE + 0x04,
FCL_IODATA_IN = FCL_BASE + 0x08,
FCL_IODATA_OUT = FCL_BASE + 0x0C,
FCL_EN = FCL_BASE + 0x10,
FCL_TIMER_0 = FCL_BASE + 0x14,
FCL_TIMER_1 = FCL_BASE + 0x18,
FCL_CLK_DIV = FCL_BASE + 0x1C,
FCL_IRQ = FCL_BASE + 0x20,
FCL_TIMER_CTRL = FCL_BASE + 0x24,
FCL_IM = FCL_BASE + 0x28,
FCL_TIMER2_0 = FCL_BASE + 0x2C,
FCL_TIMER2_1 = FCL_BASE + 0x30,
FCL_DBG_STS = FCL_BASE + 0x34,
FCL_FIFO = 0xE00,
PCI_SYS_CFG_SYSTEM = 0x800
};
/**
* struct spec_dev - SPEC instance
* It describes a SPEC device instance.
* @cdev Char device descriptor
* @dev Linux device instance descriptor
* @flags collection of bit flags
* @remap ioremap of PCI bar 0, 2, 4
* @lock it protects: flags
* @buf buffer where store FPGA bitstreams
* @size buffer size
*/
struct spec_dev {
struct cdev cdev;
struct device dev;
DECLARE_BITMAP(flags, SPEC_FLAG_BITS);
void __iomem *remap[3]; /* ioremap of bar 0, 2, 4 */
struct spinlock lock;
void *buf;
size_t size;
};
/**
* It gets a SPEC device instance
* @ptr pointer to a Linux device instance
* Return: the SPEC device instance correponding to the given Linux device
*/
static inline struct spec_dev *to_spec_dev(struct device *ptr)
{
return container_of(ptr, struct spec_dev, dev);
}
#endif /* __SPEC_H__ */
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment