474 lines
14 KiB
C
474 lines
14 KiB
C
/*
|
|
* PCIDEV: PCI host device mapping
|
|
* Copyright (C) 2003 Frank Cornelis <fcorneli@pandora.be>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
/*
|
|
* Create the PCIDEV using:
|
|
* mknod /dev/pcidev c 240 0
|
|
*/
|
|
#include <linux/module.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/init.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/mm.h>
|
|
#include <linux/slab.h>
|
|
#include <asm/uaccess.h>
|
|
#include <linux/pci.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/version.h>
|
|
|
|
#include "kernel_pcidev.h"
|
|
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_AUTHOR("Frank Cornelis <fcorneli@pandora.be>");
|
|
MODULE_DESCRIPTION("PCI Host Device Mapper Driver");
|
|
MODULE_SUPPORTED_DEVICE("pcidev");
|
|
|
|
//EXPORT_NO_SYMBOLS;
|
|
|
|
|
|
static char *pcidev_name = PCIDEV_NAME;
|
|
|
|
static int pcidev_open(struct inode *inode, struct file *file)
|
|
{
|
|
struct pcidev_struct *pcidev = kmalloc(sizeof(struct pcidev_struct), GFP_KERNEL);
|
|
pcidev->dev = NULL;
|
|
pcidev->pid = 0;
|
|
init_timer(&pcidev->irq_timer);
|
|
pcidev->irq_timer.function = NULL; // no test irq signaling
|
|
file->private_data = pcidev;
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int pcidev_release(struct inode *inode, struct file *file)
|
|
{
|
|
struct pcidev_struct *pcidev = (struct pcidev_struct *)file->private_data;
|
|
int idx;
|
|
if (!pcidev->dev)
|
|
return 0;
|
|
if (pcidev->irq_timer.function)
|
|
del_timer_sync(&pcidev->irq_timer);
|
|
if (pcidev->pid) {
|
|
u8 irq;
|
|
pci_read_config_byte(pcidev->dev, PCI_INTERRUPT_PIN, &irq);
|
|
pci_read_config_byte(pcidev->dev, PCI_INTERRUPT_LINE, &irq);
|
|
free_irq(irq, (void *)pcidev->pid);
|
|
}
|
|
for (idx = 0; idx < PCIDEV_COUNT_RESOURCES; idx++)
|
|
if (pcidev->mapped_mem[idx])
|
|
iounmap(pcidev->mapped_mem[idx]);
|
|
pci_release_regions(pcidev->dev);
|
|
kfree(file->private_data);
|
|
return 0;
|
|
}
|
|
|
|
|
|
#if (LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 0))
|
|
// linux kernel 2.4
|
|
typedef void irqreturn_t;
|
|
#define IRQ_NONE
|
|
#endif
|
|
|
|
|
|
static irqreturn_t pcidev_irqhandler(int irq, void *dev_id, struct pt_regs *regs)
|
|
{
|
|
int pid = (int)dev_id;
|
|
struct task_struct *task;
|
|
read_lock(&tasklist_lock);
|
|
task = find_task_by_pid(pid);
|
|
if (task)
|
|
send_sig_info(SIGUSR1, (struct siginfo *)1, task); // XXX: should be converted to real-time signals
|
|
read_unlock(&tasklist_lock);
|
|
return IRQ_NONE;
|
|
/*
|
|
* we cannot possible say IRQ_HANDLED because we simply
|
|
* don't know yet... only bochs could tell
|
|
*/
|
|
}
|
|
|
|
|
|
static void irq_test_timer(unsigned long data)
|
|
{
|
|
struct task_struct *task;
|
|
struct pcidev_struct *pcidev = (struct pcidev_struct *)data;
|
|
read_lock(&tasklist_lock);
|
|
task = find_task_by_pid(pcidev->pid);
|
|
if (task)
|
|
send_sig_info(SIGUSR1, (struct siginfo *)1, task);
|
|
read_unlock(&tasklist_lock);
|
|
|
|
//printk(KERN_INFO "irq_test_timer\n");
|
|
init_timer(&pcidev->irq_timer);
|
|
pcidev->irq_timer.data = data;
|
|
pcidev->irq_timer.function = irq_test_timer;
|
|
pcidev->irq_timer.expires = jiffies + HZ;
|
|
add_timer(&pcidev->irq_timer);
|
|
}
|
|
|
|
|
|
static int pcidev_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
|
|
{
|
|
int ret = 0;
|
|
struct pcidev_struct *pcidev = (struct pcidev_struct *)file->private_data;
|
|
switch(cmd) {
|
|
case PCIDEV_IOCTL_FIND: {
|
|
struct pcidev_find_struct *find;
|
|
struct pci_dev *dev;
|
|
unsigned long vendorID, deviceID;
|
|
int idx;
|
|
if (pcidev->dev)
|
|
return -EIO; // only alloc once for now
|
|
if (!access_ok(VERIFY_WRITE, (void *)arg, sizeof(struct pcidev_find_struct)))
|
|
return -EFAULT;
|
|
find = (struct pcidev_find_struct *)arg;
|
|
__get_user(vendorID, &find->vendorID);
|
|
__get_user(deviceID, &find->deviceID);
|
|
__put_user(-1, &find->bus);
|
|
__put_user(-1, &find->device);
|
|
__put_user(-1, &find->func);
|
|
dev = pci_find_device(vendorID, deviceID, NULL);
|
|
if (!dev)
|
|
return -ENOENT;
|
|
if (pci_enable_device(dev)) {
|
|
printk(KERN_WARNING "pcidev: Could not enable the PCI device.\n");
|
|
return -EIO;
|
|
}
|
|
if (pci_set_dma_mask(dev, 0xffffffff))
|
|
printk(KERN_WARNING "pcidev: only limited PCI busmaster DMA support.\n");
|
|
pci_set_master(dev);
|
|
printk(KERN_INFO "pcidev: device found at %x:%x.%d\n",
|
|
dev->bus->number, PCI_SLOT(dev->devfn),
|
|
PCI_FUNC(dev->devfn));
|
|
ret = pci_request_regions(dev, pcidev_name);
|
|
if (ret < 0)
|
|
break;
|
|
for (idx = 0; idx < PCIDEV_COUNT_RESOURCES; idx++) {
|
|
if (pci_resource_flags(dev, idx) & IORESOURCE_MEM) {
|
|
long len = pci_resource_len(dev,idx);
|
|
unsigned long mapped_start = (unsigned long)ioremap(pci_resource_start(dev, idx), len);
|
|
__put_user(mapped_start, &find->resources[idx].start);
|
|
__put_user(mapped_start + len - 1, &find->resources[idx].end);
|
|
pcidev->mapped_mem[idx] = (void *)mapped_start;
|
|
}
|
|
else {
|
|
pcidev->mapped_mem[idx] = NULL;
|
|
__put_user(pci_resource_start(dev, idx), &find->resources[idx].start);
|
|
__put_user(pci_resource_end(dev, idx), &find->resources[idx].end);
|
|
}
|
|
__put_user(pci_resource_flags(dev, idx), &find->resources[idx].flags);
|
|
}
|
|
pcidev->dev = dev;
|
|
__put_user(dev->bus->number, &find->bus);
|
|
__put_user(PCI_SLOT(dev->devfn), &find->device);
|
|
__put_user(PCI_FUNC(dev->devfn), &find->func);
|
|
ret = 0;
|
|
break;
|
|
}
|
|
case PCIDEV_IOCTL_READ_CONFIG_BYTE:
|
|
case PCIDEV_IOCTL_READ_CONFIG_WORD:
|
|
case PCIDEV_IOCTL_READ_CONFIG_DWORD: {
|
|
struct pcidev_io_struct *io;
|
|
unsigned long address, value;
|
|
if (!pcidev->dev)
|
|
return -EIO;
|
|
if (!access_ok(VERIFY_WRITE, (void *)arg, sizeof(struct pcidev_io_struct)))
|
|
return -EFAULT;
|
|
io = (struct pcidev_io_struct *)arg;
|
|
__get_user(address, &io->address);
|
|
__put_user(-1, &io->value);
|
|
printk(KERN_DEBUG "pcidev: reading config address %#x\n", (int)address);
|
|
switch(cmd) {
|
|
case PCIDEV_IOCTL_READ_CONFIG_BYTE:
|
|
ret = pci_read_config_byte(pcidev->dev, address, (u8 *)&value);
|
|
break;
|
|
case PCIDEV_IOCTL_READ_CONFIG_WORD:
|
|
ret = pci_read_config_word(pcidev->dev, address, (u16 *)&value);
|
|
break;
|
|
case PCIDEV_IOCTL_READ_CONFIG_DWORD:
|
|
ret = pci_read_config_dword(pcidev->dev, address, (u32 *)&value);
|
|
break;
|
|
}
|
|
if (ret < 0)
|
|
return ret;
|
|
__put_user(value, &io->value);
|
|
break;
|
|
}
|
|
case PCIDEV_IOCTL_WRITE_CONFIG_BYTE:
|
|
case PCIDEV_IOCTL_WRITE_CONFIG_WORD:
|
|
case PCIDEV_IOCTL_WRITE_CONFIG_DWORD: {
|
|
struct pcidev_io_struct *io;
|
|
unsigned long address, value;
|
|
if (!pcidev->dev)
|
|
return -EIO;
|
|
if (!access_ok(VERIFY_READ, (void *)arg, sizeof(struct pcidev_io_struct)))
|
|
return -EFAULT;
|
|
io = (struct pcidev_io_struct *)arg;
|
|
__get_user(address, &io->address);
|
|
__get_user(value, &io->value);
|
|
/*
|
|
* Next tests prevent the pcidev user from remapping
|
|
* the PCI host device since this could cause great
|
|
* trouble because we don't own those I/O resources.
|
|
* If the pcidev wants to remap a device he needs to
|
|
* emulate the mapping himself and not bother us about it.
|
|
*/
|
|
if (address == PCI_INTERRUPT_PIN) {
|
|
printk(KERN_WARNING "pcidev: not allowed to set irq pin!\n");
|
|
return -EIO;
|
|
}
|
|
if (address == PCI_INTERRUPT_LINE) {
|
|
printk(KERN_WARNING "pcidev: not allowed to set irq line!\n");
|
|
return -EIO;
|
|
}
|
|
if (PCI_BASE_ADDRESS_0 <= address && (address & ~3UL) <= PCI_BASE_ADDRESS_5) {
|
|
printk(KERN_WARNING "pcidev: now allowed to change base address %d\n",
|
|
(int)((address & ~3UL) - PCI_BASE_ADDRESS_0) / 4);
|
|
return -EIO;
|
|
}
|
|
printk(KERN_DEBUG "pcidev: writing config address %#x\n", (int)address);
|
|
switch(cmd) {
|
|
case PCIDEV_IOCTL_WRITE_CONFIG_BYTE:
|
|
ret = pci_write_config_byte(pcidev->dev, address, (u8)value);
|
|
break;
|
|
case PCIDEV_IOCTL_WRITE_CONFIG_WORD:
|
|
ret = pci_write_config_word(pcidev->dev, address, (u16)value);
|
|
break;
|
|
case PCIDEV_IOCTL_WRITE_CONFIG_DWORD:
|
|
ret = pci_write_config_dword(pcidev->dev, address, (u32)value);
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case PCIDEV_IOCTL_INTERRUPT: {
|
|
u8 irq;
|
|
if (!pcidev->dev)
|
|
return -EIO;
|
|
ret = pci_read_config_byte(pcidev->dev, PCI_INTERRUPT_PIN, &irq);
|
|
if (ret < 0)
|
|
break;
|
|
ret = -EIO;
|
|
if (!irq)
|
|
break;
|
|
ret = pci_read_config_byte(pcidev->dev, PCI_INTERRUPT_LINE, &irq);
|
|
if (ret < 0)
|
|
break;
|
|
if (arg & 1) {
|
|
pcidev->pid = current->pid; // our dev_id
|
|
printk(KERN_INFO "pcidev: enabling IRQ %d\n", irq);
|
|
ret = request_irq(irq, pcidev_irqhandler, SA_SHIRQ,
|
|
pcidev_name, (void *)current->pid);
|
|
}
|
|
else {
|
|
ret = -EIO;
|
|
if (!pcidev->pid)
|
|
break;
|
|
printk(KERN_INFO "pcidev: disabling IRQ %d\n", irq);
|
|
free_irq(irq, (void *)pcidev->pid);
|
|
pcidev->pid = 0;
|
|
ret = 0;
|
|
}
|
|
break;
|
|
}
|
|
/*
|
|
* Next ioctl is only for testing purposes.
|
|
*/
|
|
case PCIDEV_IOCTL_INTERRUPT_TEST: {
|
|
ret = -EIO;
|
|
if (!pcidev->dev)
|
|
break;
|
|
if (!pcidev->pid)
|
|
break;
|
|
if (pcidev->irq_timer.function)
|
|
del_timer_sync(&pcidev->irq_timer);
|
|
pcidev->irq_timer.function = NULL;
|
|
if (arg & 1) {
|
|
init_timer(&pcidev->irq_timer);
|
|
pcidev->irq_timer.function = irq_test_timer;
|
|
pcidev->irq_timer.data = (unsigned long)pcidev;
|
|
pcidev->irq_timer.expires = jiffies + HZ;
|
|
add_timer(&pcidev->irq_timer);
|
|
}
|
|
ret = 0;
|
|
break;
|
|
}
|
|
case PCIDEV_IOCTL_READ_IO_BYTE:
|
|
case PCIDEV_IOCTL_READ_IO_WORD:
|
|
case PCIDEV_IOCTL_READ_IO_DWORD: {
|
|
/*
|
|
* We should probably check access rights against
|
|
* the PCI resource list... but who cares for a
|
|
* security hole more or less :)
|
|
*/
|
|
struct pcidev_io_struct *io;
|
|
unsigned long address, value = -1;
|
|
if (!access_ok(VERIFY_WRITE, (void *)arg, sizeof(struct pcidev_io_struct)))
|
|
return -EFAULT;
|
|
io = (struct pcidev_io_struct *)arg;
|
|
__get_user(address, &io->address);
|
|
printk(KERN_DEBUG "pcidev: reading I/O port %#x\n", (int)address);
|
|
switch(cmd) {
|
|
case PCIDEV_IOCTL_READ_IO_BYTE:
|
|
value = inb(address);
|
|
break;
|
|
case PCIDEV_IOCTL_READ_IO_WORD:
|
|
value = inw(address);
|
|
break;
|
|
case PCIDEV_IOCTL_READ_IO_DWORD:
|
|
value = inl(address);
|
|
break;
|
|
}
|
|
__put_user(value, &io->value);
|
|
ret = 0;
|
|
break;
|
|
}
|
|
case PCIDEV_IOCTL_WRITE_IO_BYTE:
|
|
case PCIDEV_IOCTL_WRITE_IO_WORD:
|
|
case PCIDEV_IOCTL_WRITE_IO_DWORD: {
|
|
struct pcidev_io_struct *io;
|
|
unsigned long address, value;
|
|
if (!access_ok(VERIFY_READ, (void *)arg, sizeof(struct pcidev_io_struct)))
|
|
return -EFAULT;
|
|
io = (struct pcidev_io_struct *)arg;
|
|
__get_user(address, &io->address);
|
|
__get_user(value, &io->value);
|
|
printk(KERN_DEBUG "pcidev: writing I/O port %#x\n", (int)address);
|
|
switch(cmd) {
|
|
case PCIDEV_IOCTL_WRITE_IO_BYTE:
|
|
outb(value, address);
|
|
break;
|
|
case PCIDEV_IOCTL_WRITE_IO_WORD:
|
|
outw(value, address);
|
|
break;
|
|
case PCIDEV_IOCTL_WRITE_IO_DWORD:
|
|
outl(value, address);
|
|
break;
|
|
}
|
|
ret = 0;
|
|
break;
|
|
}
|
|
case PCIDEV_IOCTL_READ_MEM_BYTE:
|
|
case PCIDEV_IOCTL_READ_MEM_WORD:
|
|
case PCIDEV_IOCTL_READ_MEM_DWORD: {
|
|
struct pcidev_io_struct *io;
|
|
unsigned long address, value = -1;
|
|
if (!access_ok(VERIFY_WRITE, (void *)arg, sizeof(struct pcidev_io_struct)))
|
|
return -EFAULT;
|
|
io = (struct pcidev_io_struct *)arg;
|
|
__get_user(address, &io->address);
|
|
printk(KERN_DEBUG "pcidev: reading memory %#x\n", (int)address);
|
|
switch(cmd) {
|
|
case PCIDEV_IOCTL_READ_MEM_BYTE:
|
|
value = readb(address);
|
|
break;
|
|
case PCIDEV_IOCTL_READ_MEM_WORD:
|
|
value = readw(address);
|
|
break;
|
|
case PCIDEV_IOCTL_READ_MEM_DWORD:
|
|
value = readl(address);
|
|
break;
|
|
}
|
|
__put_user(value, &io->value);
|
|
ret = 0;
|
|
break;
|
|
}
|
|
case PCIDEV_IOCTL_WRITE_MEM_BYTE:
|
|
case PCIDEV_IOCTL_WRITE_MEM_WORD:
|
|
case PCIDEV_IOCTL_WRITE_MEM_DWORD: {
|
|
struct pcidev_io_struct *io;
|
|
unsigned long address, value;
|
|
if (!access_ok(VERIFY_READ, (void *)arg, sizeof(struct pcidev_io_struct)))
|
|
return -EFAULT;
|
|
io = (struct pcidev_io_struct *)arg;
|
|
__get_user(address, &io->address);
|
|
__get_user(value, &io->value);
|
|
printk(KERN_DEBUG "pcidev: writing memory %#x\n", (int)address);
|
|
switch(cmd) {
|
|
case PCIDEV_IOCTL_WRITE_MEM_BYTE:
|
|
writeb(value, address);
|
|
break;
|
|
case PCIDEV_IOCTL_WRITE_MEM_WORD:
|
|
writew(value, address);
|
|
break;
|
|
case PCIDEV_IOCTL_WRITE_MEM_DWORD:
|
|
writel(value, address);
|
|
break;
|
|
}
|
|
ret = 0;
|
|
break;
|
|
}
|
|
case PCIDEV_IOCTL_PROBE_CONFIG_DWORD: {
|
|
/*
|
|
* This ioctl allows for probing a config space value.
|
|
* This can be used for base address size probing
|
|
*/
|
|
struct pcidev_io_struct *io;
|
|
unsigned long address, value, orig_value;
|
|
if (!pcidev->dev)
|
|
return -EIO;
|
|
if (!access_ok(VERIFY_WRITE, (void *)arg, sizeof(struct pcidev_io_struct)))
|
|
return -EFAULT;
|
|
io = (struct pcidev_io_struct *)arg;
|
|
__get_user(address, &io->address);
|
|
__get_user(value, &io->value);
|
|
__put_user(-1, &io->value);
|
|
printk(KERN_INFO "pcidev: probing config space address: %#x\n", (int)address);
|
|
ret = pci_read_config_dword(pcidev->dev, address, (u32 *)&orig_value);
|
|
if (ret < 0)
|
|
break;
|
|
pci_write_config_dword(pcidev->dev, address, (u32)value);
|
|
pci_read_config_dword(pcidev->dev, address, (u32 *)&value);
|
|
ret = pci_write_config_dword(pcidev->dev, address, (u32)orig_value);
|
|
if (ret < 0)
|
|
break;
|
|
__put_user(value, &io->value);
|
|
break;
|
|
}
|
|
default:
|
|
ret = -ENOTTY;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
static struct file_operations pcidev_fops = {
|
|
open: pcidev_open,
|
|
release: pcidev_release,
|
|
ioctl: pcidev_ioctl,
|
|
owner: THIS_MODULE
|
|
};
|
|
|
|
|
|
|
|
int __init init_module(void)
|
|
{
|
|
int result;
|
|
printk(KERN_INFO "pcidev init\n");
|
|
if ((result = register_chrdev(PCIDEV_MAJOR, pcidev_name, &pcidev_fops)) < 0) {
|
|
printk(KERN_WARNING "Could not register pcidev.\n");
|
|
return result;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
void __exit cleanup_module(void)
|
|
{
|
|
if (unregister_chrdev(PCIDEV_MAJOR, pcidev_name) < 0)
|
|
printk(KERN_WARNING "Could not unregister pcidev.\n");
|
|
printk(KERN_INFO "pcidev cleanup\n");
|
|
}
|