diff options
-rw-r--r-- | drivers/misc/Kconfig | 1 | ||||
-rw-r--r-- | drivers/misc/Makefile | 1 | ||||
-rw-r--r-- | drivers/misc/habanalabs/Kconfig | 22 | ||||
-rw-r--r-- | drivers/misc/habanalabs/Makefile | 7 | ||||
-rw-r--r-- | drivers/misc/habanalabs/device.c | 340 | ||||
-rw-r--r-- | drivers/misc/habanalabs/habanalabs.h | 131 | ||||
-rw-r--r-- | drivers/misc/habanalabs/habanalabs_drv.c | 358 |
7 files changed, 860 insertions, 0 deletions
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig index 7e0726253755..f3e257fd3704 100644 --- a/drivers/misc/Kconfig +++ b/drivers/misc/Kconfig @@ -545,4 +545,5 @@ source "drivers/misc/echo/Kconfig" source "drivers/misc/cxl/Kconfig" source "drivers/misc/ocxl/Kconfig" source "drivers/misc/cardreader/Kconfig" +source "drivers/misc/habanalabs/Kconfig" endmenu diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile index 623d002c59ce..d5b7d3404dc7 100644 --- a/drivers/misc/Makefile +++ b/drivers/misc/Makefile @@ -60,3 +60,4 @@ obj-$(CONFIG_PCI_ENDPOINT_TEST) += pci_endpoint_test.o obj-$(CONFIG_OCXL) += ocxl/ obj-y += cardreader/ obj-$(CONFIG_PVPANIC) += pvpanic.o +obj-$(CONFIG_HABANA_AI) += habanalabs/ diff --git a/drivers/misc/habanalabs/Kconfig b/drivers/misc/habanalabs/Kconfig new file mode 100644 index 000000000000..b7f38a14caf5 --- /dev/null +++ b/drivers/misc/habanalabs/Kconfig @@ -0,0 +1,22 @@ +# +# HabanaLabs AI accelerators driver +# + +config HABANA_AI + tristate "HabanaAI accelerators (habanalabs)" + depends on PCI + select FRAME_VECTOR + help + Enables PCIe card driver for Habana's AI Processors (AIP) that are + designed to accelerate Deep Learning inference and training workloads. + + The driver manages the PCIe devices and provides IOCTL interface for + the user to submit workloads to the devices. + + The user-space interface is described in + include/uapi/misc/habanalabs.h + + If unsure, say N. + + To compile this driver as a module, choose M here: the + module will be called habanalabs. diff --git a/drivers/misc/habanalabs/Makefile b/drivers/misc/habanalabs/Makefile new file mode 100644 index 000000000000..0910f7fa34ec --- /dev/null +++ b/drivers/misc/habanalabs/Makefile @@ -0,0 +1,7 @@ +# +# Makefile for HabanaLabs AI accelerators driver +# + +obj-m := habanalabs.o + +habanalabs-y := habanalabs_drv.o device.o diff --git a/drivers/misc/habanalabs/device.c b/drivers/misc/habanalabs/device.c new file mode 100644 index 000000000000..6189fd0e2ccd --- /dev/null +++ b/drivers/misc/habanalabs/device.c @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: GPL-2.0 + +/* + * Copyright 2016-2019 HabanaLabs, Ltd. + * All Rights Reserved. + */ + +#include "habanalabs.h" + +#include <linux/pci.h> +#include <linux/delay.h> + +static void hpriv_release(struct kref *ref) +{ + struct hl_fpriv *hpriv; + struct hl_device *hdev; + + hpriv = container_of(ref, struct hl_fpriv, refcount); + + hdev = hpriv->hdev; + + put_pid(hpriv->taskpid); + + kfree(hpriv); +} + +void hl_hpriv_get(struct hl_fpriv *hpriv) +{ + kref_get(&hpriv->refcount); +} + +void hl_hpriv_put(struct hl_fpriv *hpriv) +{ + kref_put(&hpriv->refcount, hpriv_release); +} + +/* + * hl_device_release - release function for habanalabs device + * + * @inode: pointer to inode structure + * @filp: pointer to file structure + * + * Called when process closes an habanalabs device + */ +static int hl_device_release(struct inode *inode, struct file *filp) +{ + struct hl_fpriv *hpriv = filp->private_data; + + filp->private_data = NULL; + + hl_hpriv_put(hpriv); + + return 0; +} + +static const struct file_operations hl_ops = { + .owner = THIS_MODULE, + .open = hl_device_open, + .release = hl_device_release +}; + +/* + * device_setup_cdev - setup cdev and device for habanalabs device + * + * @hdev: pointer to habanalabs device structure + * @hclass: pointer to the class object of the device + * @minor: minor number of the specific device + * @fpos : file operations to install for this device + * + * Create a cdev and a Linux device for habanalabs's device. Need to be + * called at the end of the habanalabs device initialization process, + * because this function exposes the device to the user + */ +static int device_setup_cdev(struct hl_device *hdev, struct class *hclass, + int minor, const struct file_operations *fops) +{ + int err, devno = MKDEV(hdev->major, minor); + struct cdev *hdev_cdev = &hdev->cdev; + char *name; + + name = kasprintf(GFP_KERNEL, "hl%d", hdev->id); + if (!name) + return -ENOMEM; + + cdev_init(hdev_cdev, fops); + hdev_cdev->owner = THIS_MODULE; + err = cdev_add(hdev_cdev, devno, 1); + if (err) { + pr_err("Failed to add char device %s\n", name); + goto err_cdev_add; + } + + hdev->dev = device_create(hclass, NULL, devno, NULL, "%s", name); + if (IS_ERR(hdev->dev)) { + pr_err("Failed to create device %s\n", name); + err = PTR_ERR(hdev->dev); + goto err_device_create; + } + + dev_set_drvdata(hdev->dev, hdev); + + kfree(name); + + return 0; + +err_device_create: + cdev_del(hdev_cdev); +err_cdev_add: + kfree(name); + return err; +} + +/* + * device_early_init - do some early initialization for the habanalabs device + * + * @hdev: pointer to habanalabs device structure + * + * Install the relevant function pointers and call the early_init function, + * if such a function exists + */ +static int device_early_init(struct hl_device *hdev) +{ + switch (hdev->asic_type) { + case ASIC_GOYA: + strlcpy(hdev->asic_name, "GOYA", sizeof(hdev->asic_name)); + break; + default: + dev_err(hdev->dev, "Unrecognized ASIC type %d\n", + hdev->asic_type); + return -EINVAL; + } + + return 0; +} + +/* + * device_early_fini - finalize all that was done in device_early_init + * + * @hdev: pointer to habanalabs device structure + * + */ +static void device_early_fini(struct hl_device *hdev) +{ +} + +/* + * hl_device_suspend - initiate device suspend + * + * @hdev: pointer to habanalabs device structure + * + * Puts the hw in the suspend state (all asics). + * Returns 0 for success or an error on failure. + * Called at driver suspend. + */ +int hl_device_suspend(struct hl_device *hdev) +{ + pci_save_state(hdev->pdev); + + /* Shut down the device */ + pci_disable_device(hdev->pdev); + pci_set_power_state(hdev->pdev, PCI_D3hot); + + return 0; +} + +/* + * hl_device_resume - initiate device resume + * + * @hdev: pointer to habanalabs device structure + * + * Bring the hw back to operating state (all asics). + * Returns 0 for success or an error on failure. + * Called at driver resume. + */ +int hl_device_resume(struct hl_device *hdev) +{ + int rc; + + pci_set_power_state(hdev->pdev, PCI_D0); + pci_restore_state(hdev->pdev); + rc = pci_enable_device(hdev->pdev); + if (rc) { + dev_err(hdev->dev, + "Failed to enable PCI device in resume\n"); + return rc; + } + + return 0; +} + +/* + * hl_device_init - main initialization function for habanalabs device + * + * @hdev: pointer to habanalabs device structure + * + * Allocate an id for the device, do early initialization and then call the + * ASIC specific initialization functions. Finally, create the cdev and the + * Linux device to expose it to the user + */ +int hl_device_init(struct hl_device *hdev, struct class *hclass) +{ + int rc; + + /* Create device */ + rc = device_setup_cdev(hdev, hclass, hdev->id, &hl_ops); + + if (rc) + goto out_disabled; + + /* Initialize ASIC function pointers and perform early init */ + rc = device_early_init(hdev); + if (rc) + goto release_device; + + dev_notice(hdev->dev, + "Successfully added device to habanalabs driver\n"); + + return 0; + +release_device: + device_destroy(hclass, hdev->dev->devt); + cdev_del(&hdev->cdev); +out_disabled: + hdev->disabled = true; + if (hdev->pdev) + dev_err(&hdev->pdev->dev, + "Failed to initialize hl%d. Device is NOT usable !\n", + hdev->id); + else + pr_err("Failed to initialize hl%d. Device is NOT usable !\n", + hdev->id); + + return rc; +} + +/* + * hl_device_fini - main tear-down function for habanalabs device + * + * @hdev: pointer to habanalabs device structure + * + * Destroy the device, call ASIC fini functions and release the id + */ +void hl_device_fini(struct hl_device *hdev) +{ + dev_info(hdev->dev, "Removing device\n"); + + /* Mark device as disabled */ + hdev->disabled = true; + + device_early_fini(hdev); + + /* Hide device from user */ + device_destroy(hdev->dev->class, hdev->dev->devt); + cdev_del(&hdev->cdev); + + pr_info("removed device successfully\n"); +} + +/* + * hl_poll_timeout_memory - Periodically poll a host memory address + * until it is not zero or a timeout occurs + * @hdev: pointer to habanalabs device structure + * @addr: Address to poll + * @timeout_us: timeout in us + * @val: Variable to read the value into + * + * Returns 0 on success and -ETIMEDOUT upon a timeout. In either + * case, the last read value at @addr is stored in @val. Must not + * be called from atomic context if sleep_us or timeout_us are used. + * + * The function sleeps for 100us with timeout value of + * timeout_us + */ +int hl_poll_timeout_memory(struct hl_device *hdev, u64 addr, + u32 timeout_us, u32 *val) +{ + /* + * address in this function points always to a memory location in the + * host's (server's) memory. That location is updated asynchronously + * either by the direct access of the device or by another core + */ + u32 *paddr = (u32 *) (uintptr_t) addr; + ktime_t timeout = ktime_add_us(ktime_get(), timeout_us); + + might_sleep(); + + for (;;) { + /* + * Flush CPU read/write buffers to make sure we read updates + * done by other cores or by the device + */ + mb(); + *val = *paddr; + if (*val) + break; + if (ktime_compare(ktime_get(), timeout) > 0) { + *val = *paddr; + break; + } + usleep_range((100 >> 2) + 1, 100); + } + + return *val ? 0 : -ETIMEDOUT; +} + +/* + * hl_poll_timeout_devicememory - Periodically poll a device memory address + * until it is not zero or a timeout occurs + * @hdev: pointer to habanalabs device structure + * @addr: Device address to poll + * @timeout_us: timeout in us + * @val: Variable to read the value into + * + * Returns 0 on success and -ETIMEDOUT upon a timeout. In either + * case, the last read value at @addr is stored in @val. Must not + * be called from atomic context if sleep_us or timeout_us are used. + * + * The function sleeps for 100us with timeout value of + * timeout_us + */ +int hl_poll_timeout_device_memory(struct hl_device *hdev, void __iomem *addr, + u32 timeout_us, u32 *val) +{ + ktime_t timeout = ktime_add_us(ktime_get(), timeout_us); + + might_sleep(); + + for (;;) { + *val = readl(addr); + if (*val) + break; + if (ktime_compare(ktime_get(), timeout) > 0) { + *val = readl(addr); + break; + } + usleep_range((100 >> 2) + 1, 100); + } + + return *val ? 0 : -ETIMEDOUT; +} diff --git a/drivers/misc/habanalabs/habanalabs.h b/drivers/misc/habanalabs/habanalabs.h new file mode 100644 index 000000000000..fa628b05db13 --- /dev/null +++ b/drivers/misc/habanalabs/habanalabs.h @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: GPL-2.0 + * + * Copyright 2016-2019 HabanaLabs, Ltd. + * All Rights Reserved. + * + */ + +#ifndef HABANALABSP_H_ +#define HABANALABSP_H_ + +#define pr_fmt(fmt) "habanalabs: " fmt + +#include <linux/cdev.h> + +#define HL_NAME "habanalabs" + +struct hl_device; + + +/* + * ASICs + */ + +/** + * enum hl_asic_type - supported ASIC types. + * @ASIC_AUTO_DETECT: ASIC type will be automatically set. + * @ASIC_GOYA: Goya device. + * @ASIC_INVALID: Invalid ASIC type. + */ +enum hl_asic_type { + ASIC_AUTO_DETECT, + ASIC_GOYA, + ASIC_INVALID +}; + + +/* + * FILE PRIVATE STRUCTURE + */ + +/** + * struct hl_fpriv - process information stored in FD private data. + * @hdev: habanalabs device structure. + * @filp: pointer to the given file structure. + * @taskpid: current process ID. + * @refcount: number of related contexts. + */ +struct hl_fpriv { + struct hl_device *hdev; + struct file *filp; + struct pid *taskpid; + struct kref refcount; +}; + + +/* + * DEVICES + */ + +/* Theoretical limit only. A single host can only contain up to 4 or 8 PCIe + * x16 cards. In extereme cases, there are hosts that can accommodate 16 cards + */ +#define HL_MAX_MINORS 256 + +/** + * struct hl_device - habanalabs device structure. + * @pdev: pointer to PCI device, can be NULL in case of simulator device. + * @cdev: related char device. + * @dev: realted kernel basic device structure. + * @asic_name: ASIC specific nmae. + * @asic_type: ASIC specific type. + * @major: habanalabs KMD major. + * @id: device minor. + * @disabled: is device disabled. + */ +struct hl_device { + struct pci_dev *pdev; + struct cdev cdev; + struct device *dev; + char asic_name[16]; + enum hl_asic_type asic_type; + u32 major; + u16 id; + u8 disabled; +}; + + +/* + * IOCTLs + */ + +/** + * typedef hl_ioctl_t - typedef for ioctl function in the driver + * @hpriv: pointer to the FD's private data, which contains state of + * user process + * @data: pointer to the input/output arguments structure of the IOCTL + * + * Return: 0 for success, negative value for error + */ +typedef int hl_ioctl_t(struct hl_fpriv *hpriv, void *data); + +/** + * struct hl_ioctl_desc - describes an IOCTL entry of the driver. + * @cmd: the IOCTL code as created by the kernel macros. + * @func: pointer to the driver's function that should be called for this IOCTL. + */ +struct hl_ioctl_desc { + unsigned int cmd; + hl_ioctl_t *func; +}; + + +/* + * Kernel module functions that can be accessed by entire module + */ + +int hl_device_open(struct inode *inode, struct file *filp); +int create_hdev(struct hl_device **dev, struct pci_dev *pdev, + enum hl_asic_type asic_type, int minor); +void destroy_hdev(struct hl_device *hdev); +int hl_poll_timeout_memory(struct hl_device *hdev, u64 addr, u32 timeout_us, + u32 *val); +int hl_poll_timeout_device_memory(struct hl_device *hdev, void __iomem *addr, + u32 timeout_us, u32 *val); + +int hl_device_init(struct hl_device *hdev, struct class *hclass); +void hl_device_fini(struct hl_device *hdev); +int hl_device_suspend(struct hl_device *hdev); +int hl_device_resume(struct hl_device *hdev); + +#endif /* HABANALABSP_H_ */ diff --git a/drivers/misc/habanalabs/habanalabs_drv.c b/drivers/misc/habanalabs/habanalabs_drv.c new file mode 100644 index 000000000000..a5251ed277d1 --- /dev/null +++ b/drivers/misc/habanalabs/habanalabs_drv.c @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: GPL-2.0 + +/* + * Copyright 2016-2019 HabanaLabs, Ltd. + * All Rights Reserved. + * + */ + +#include "habanalabs.h" + +#include <linux/pci.h> +#include <linux/module.h> + +#define HL_DRIVER_AUTHOR "HabanaLabs Kernel Driver Team" + +#define HL_DRIVER_DESC "Driver for HabanaLabs's AI Accelerators" + +MODULE_AUTHOR(HL_DRIVER_AUTHOR); +MODULE_DESCRIPTION(HL_DRIVER_DESC); +MODULE_LICENSE("GPL v2"); + +static int hl_major; +static struct class *hl_class; +DEFINE_IDR(hl_devs_idr); +DEFINE_MUTEX(hl_devs_idr_lock); + +#define PCI_VENDOR_ID_HABANALABS 0x1da3 + +#define PCI_IDS_GOYA 0x0001 + +static const struct pci_device_id ids[] = { + { PCI_DEVICE(PCI_VENDOR_ID_HABANALABS, PCI_IDS_GOYA), }, + { 0, } +}; +MODULE_DEVICE_TABLE(pci, ids); + +/* + * get_asic_type - translate device id to asic type + * + * @device: id of the PCI device + * + * Translate device id to asic type. + * In case of unidentified device, return -1 + */ +static enum hl_asic_type get_asic_type(u16 device) +{ + enum hl_asic_type asic_type; + + switch (device) { + case PCI_IDS_GOYA: + asic_type = ASIC_GOYA; + break; + default: + asic_type = ASIC_INVALID; + break; + } + + return asic_type; +} + +/* + * hl_device_open - open function for habanalabs device + * + * @inode: pointer to inode structure + * @filp: pointer to file structure + * + * Called when process opens an habanalabs device. + */ +int hl_device_open(struct inode *inode, struct file *filp) +{ + struct hl_device *hdev; + struct hl_fpriv *hpriv; + + mutex_lock(&hl_devs_idr_lock); + hdev = idr_find(&hl_devs_idr, iminor(inode)); + mutex_unlock(&hl_devs_idr_lock); + + if (!hdev) { + pr_err("Couldn't find device %d:%d\n", + imajor(inode), iminor(inode)); + return -ENXIO; + } + + hpriv = kzalloc(sizeof(*hpriv), GFP_KERNEL); + if (!hpriv) + return -ENOMEM; + + hpriv->hdev = hdev; + filp->private_data = hpriv; + hpriv->filp = filp; + kref_init(&hpriv->refcount); + nonseekable_open(inode, filp); + + hpriv->taskpid = find_get_pid(current->pid); + + return 0; +} + +/* + * create_hdev - create habanalabs device instance + * + * @dev: will hold the pointer to the new habanalabs device structure + * @pdev: pointer to the pci device + * @asic_type: in case of simulator device, which device is it + * @minor: in case of simulator device, the minor of the device + * + * Allocate memory for habanalabs device and initialize basic fields + * Identify the ASIC type + * Allocate ID (minor) for the device (only for real devices) + */ +int create_hdev(struct hl_device **dev, struct pci_dev *pdev, + enum hl_asic_type asic_type, int minor) +{ + struct hl_device *hdev; + int rc; + + *dev = NULL; + + hdev = kzalloc(sizeof(*hdev), GFP_KERNEL); + if (!hdev) + return -ENOMEM; + + hdev->major = hl_major; + + hdev->disabled = true; + hdev->pdev = pdev; /* can be NULL in case of simulator device */ + + if (asic_type == ASIC_AUTO_DETECT) { + hdev->asic_type = get_asic_type(pdev->device); + if (hdev->asic_type == ASIC_INVALID) { + dev_err(&pdev->dev, "Unsupported ASIC\n"); + rc = -ENODEV; + goto free_hdev; + } + } else { + hdev->asic_type = asic_type; + } + + mutex_lock(&hl_devs_idr_lock); + + if (minor == -1) { + rc = idr_alloc(&hl_devs_idr, hdev, 0, HL_MAX_MINORS, + GFP_KERNEL); + } else { + void *old_idr = idr_replace(&hl_devs_idr, hdev, minor); + + if (IS_ERR_VALUE(old_idr)) { + rc = PTR_ERR(old_idr); + pr_err("Error %d when trying to replace minor %d\n", + rc, minor); + mutex_unlock(&hl_devs_idr_lock); + goto free_hdev; + } + rc = minor; + } + + mutex_unlock(&hl_devs_idr_lock); + + if (rc < 0) { + if (rc == -ENOSPC) { + pr_err("too many devices in the system\n"); + rc = -EBUSY; + } + goto free_hdev; + } + + hdev->id = rc; + + *dev = hdev; + + return 0; + +free_hdev: + kfree(hdev); + return rc; +} + +/* + * destroy_hdev - destroy habanalabs device instance + * + * @dev: pointer to the habanalabs device structure + * + */ +void destroy_hdev(struct hl_device *hdev) +{ + /* Remove device from the device list */ + mutex_lock(&hl_devs_idr_lock); + idr_remove(&hl_devs_idr, hdev->id); + mutex_unlock(&hl_devs_idr_lock); + + kfree(hdev); +} + +static int hl_pmops_suspend(struct device *dev) +{ + struct pci_dev *pdev = to_pci_dev(dev); + struct hl_device *hdev = pci_get_drvdata(pdev); + + pr_debug("Going to suspend PCI device\n"); + + if (!hdev) { + pr_err("device pointer is NULL in suspend\n"); + return 0; + } + + return hl_device_suspend(hdev); +} + +static int hl_pmops_resume(struct device *dev) +{ + struct pci_dev *pdev = to_pci_dev(dev); + struct hl_device *hdev = pci_get_drvdata(pdev); + + pr_debug("Going to resume PCI device\n"); + + if (!hdev) { + pr_err("device pointer is NULL in resume\n"); + return 0; + } + + return hl_device_resume(hdev); +} + +/* + * hl_pci_probe - probe PCI habanalabs devices + * + * @pdev: pointer to pci device + * @id: pointer to pci device id structure + * + * Standard PCI probe function for habanalabs device. + * Create a new habanalabs device and initialize it according to the + * device's type + */ +static int hl_pci_probe(struct pci_dev *pdev, + const struct pci_device_id *id) +{ + struct hl_device *hdev; + int rc; + + dev_info(&pdev->dev, HL_NAME + " device found [%04x:%04x] (rev %x)\n", + (int)pdev->vendor, (int)pdev->device, (int)pdev->revision); + + rc = create_hdev(&hdev, pdev, ASIC_AUTO_DETECT, -1); + if (rc) + return rc; + + pci_set_drvdata(pdev, hdev); + + rc = hl_device_init(hdev, hl_class); + if (rc) { + dev_err(&pdev->dev, "Fatal error during habanalabs device init\n"); + rc = -ENODEV; + goto disable_device; + } + + return 0; + +disable_device: + pci_set_drvdata(pdev, NULL); + destroy_hdev(hdev); + + return rc; +} + +/* + * hl_pci_remove - remove PCI habanalabs devices + * + * @pdev: pointer to pci device + * + * Standard PCI remove function for habanalabs device + */ +static void hl_pci_remove(struct pci_dev *pdev) +{ + struct hl_device *hdev; + + hdev = pci_get_drvdata(pdev); + if (!hdev) + return; + + hl_device_fini(hdev); + pci_set_drvdata(pdev, NULL); + + destroy_hdev(hdev); +} + +static const struct dev_pm_ops hl_pm_ops = { + .suspend = hl_pmops_suspend, + .resume = hl_pmops_resume, +}; + +static struct pci_driver hl_pci_driver = { + .name = HL_NAME, + .id_table = ids, + .probe = hl_pci_probe, + .remove = hl_pci_remove, + .driver.pm = &hl_pm_ops, +}; + +/* + * hl_init - Initialize the habanalabs kernel driver + */ +static int __init hl_init(void) +{ + int rc; + dev_t dev; + + pr_info("loading driver\n"); + + rc = alloc_chrdev_region(&dev, 0, HL_MAX_MINORS, HL_NAME); + if (rc < 0) { + pr_err("unable to get major\n"); + return rc; + } + + hl_major = MAJOR(dev); + + hl_class = class_create(THIS_MODULE, HL_NAME); + if (IS_ERR(hl_class)) { + pr_err("failed to allocate class\n"); + rc = PTR_ERR(hl_class); + goto remove_major; + } + + rc = pci_register_driver(&hl_pci_driver); + if (rc) { + pr_err("failed to register pci device\n"); + goto remove_class; + } + + pr_debug("driver loaded\n"); + + return 0; + +remove_class: + class_destroy(hl_class); +remove_major: + unregister_chrdev_region(MKDEV(hl_major, 0), HL_MAX_MINORS); + return rc; +} + +/* + * hl_exit - Release all resources of the habanalabs kernel driver + */ +static void __exit hl_exit(void) +{ + pci_unregister_driver(&hl_pci_driver); + + class_destroy(hl_class); + unregister_chrdev_region(MKDEV(hl_major, 0), HL_MAX_MINORS); + + idr_destroy(&hl_devs_idr); + + pr_debug("driver removed\n"); +} + +module_init(hl_init); +module_exit(hl_exit); |