/*
|
* Copyright (C) 2024 LingYun IoT System Studio
|
* Author: Guo Wenxue <guowenxue@gmail.com>
|
*
|
* A character skeleton driver example in linux kernel.
|
*/
|
|
#include <linux/module.h>
|
#include <linux/init.h>
|
#include <linux/kernel.h> /* printk() */
|
#include <linux/fs.h> /* everything... */
|
#include <linux/errno.h> /* error codes */
|
#include <linux/types.h> /* size_t */
|
#include <linux/cdev.h> /* cdev */
|
#include <linux/slab.h> /* kmalloc() */
|
#include <linux/version.h> /* kernel version code */
|
#include <linux/uaccess.h> /* copy_from/to_user() */
|
#include <linux/moduleparam.h>
|
|
/* device name and major number */
|
#define DEV_NAME "chrdev"
|
int dev_major = 0;
|
module_param(dev_major, int, S_IRUGO);
|
|
#define BUF_SIZE 1024
|
typedef struct chrdev_s
|
{
|
struct cdev cdev;
|
struct class *class;
|
struct device *device;
|
char *data; /* data buffer */
|
uint32_t size; /* data buffer size */
|
uint32_t bytes; /* data bytes in the buffer */
|
} chrdev_t;
|
|
static struct chrdev_s dev;
|
|
static ssize_t chrdev_read (struct file *file, char __user *buf, size_t count, loff_t *f_pos)
|
{
|
struct chrdev_s *dev = file->private_data;
|
ssize_t nbytes;
|
ssize_t rv = 0;
|
|
/* no data in buffer */
|
if( !dev->bytes )
|
return 0;
|
|
/* copy data to user space */
|
nbytes = count>dev->bytes ? dev->bytes : count;
|
if( copy_to_user(buf, dev->data, nbytes) )
|
{
|
rv = -EFAULT;
|
goto out;
|
}
|
|
/* update return value and data bytes in buffer */
|
rv = nbytes;
|
dev->bytes -= nbytes;
|
|
out:
|
return rv;
|
}
|
|
static ssize_t chrdev_write (struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
|
{
|
struct chrdev_s *dev = file->private_data;
|
ssize_t nbytes;
|
ssize_t rv = 0;
|
|
/* no space left */
|
if( dev->bytes >= dev->size )
|
return -ENOSPC;
|
|
/* check copy data bytes */
|
if( dev->size - dev->bytes < count)
|
nbytes = dev->size - dev->bytes;
|
else
|
nbytes = count;
|
|
/* copy data from user space */
|
if( copy_from_user(&dev->data[dev->bytes], buf, nbytes) )
|
{
|
rv = -EFAULT;
|
goto out;
|
}
|
|
/* update return value and data bytes in buffer */
|
rv = nbytes;
|
dev->bytes += nbytes;
|
|
out:
|
return rv;
|
}
|
|
#if LINUX_VERSION_CODE < KERNEL_VERSION(5,0,0)
|
#define access_ok_wrapper(type,arg,cmd) access_ok(type, arg, cmd)
|
#else
|
#define access_ok_wrapper(type,arg,cmd) access_ok(arg, cmd)
|
#endif
|
|
/* ioctl definitions, use 'c' as magic number */
|
#define CHR_MAGIC 'c'
|
#define CHR_MAXNR 2
|
#define CMD_READ _IOR(CHR_MAGIC, 0, int)
|
#define CMD_WRITE _IOW(CHR_MAGIC, 1, int)
|
|
static long chrdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
|
{
|
static int value = 0xdeadbeef;
|
int rv = 0;
|
|
/*
|
* extract the type and number bitfields, and don't decode
|
* wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok()
|
*/
|
if (_IOC_TYPE(cmd) != CHR_MAGIC) return -ENOTTY;
|
if (_IOC_NR(cmd) > CHR_MAXNR) return -ENOTTY;
|
|
/*
|
* the direction is a bitmask, and VERIFY_WRITE catches R/W transfers.
|
* `Type' is user-oriented, while access_ok is kernel-oriented,
|
* so the concept of "read" and "write" is reversed
|
*/
|
if (_IOC_DIR(cmd) & _IOC_READ)
|
rv = !access_ok_wrapper(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
|
else if (_IOC_DIR(cmd) & _IOC_WRITE)
|
rv = !access_ok_wrapper(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
|
|
if (rv)
|
return -EFAULT;
|
|
switch (cmd) {
|
case CMD_READ:
|
if (copy_to_user((int __user *)arg, &value, sizeof(value)))
|
return -EFAULT;
|
break;
|
|
case CMD_WRITE:
|
if (copy_from_user(&value, (int __user *)arg, sizeof(value)))
|
return -EFAULT;
|
break;
|
|
default:
|
return -EINVAL;
|
}
|
|
return 0;
|
}
|
|
static int chrdev_open (struct inode *inode, struct file *file)
|
{
|
struct chrdev_s *dev; /* device struct address */
|
|
/* get the device struct address by container_of() */
|
dev = container_of(inode->i_cdev, struct chrdev_s, cdev);
|
|
/* save the device struct address for other methods */
|
file->private_data = dev;
|
|
return 0;
|
}
|
|
static int chrdev_close (struct inode *node, struct file *file)
|
{
|
return 0;
|
}
|
|
static struct file_operations chrdev_fops = {
|
.owner = THIS_MODULE,
|
.open = chrdev_open, /* open() implementation */
|
.read = chrdev_read, /* read() implementation */
|
.write = chrdev_write, /* write() implementation */
|
.unlocked_ioctl = chrdev_ioctl, /* ioctl() implementation */
|
.release = chrdev_close, /* close() implementation */
|
};
|
|
static int __init chrdev_init(void)
|
{
|
dev_t devno;
|
int rv;
|
|
/* malloc and initial device read/write buffer */
|
dev.data = kmalloc(BUF_SIZE, GFP_KERNEL);
|
if( !dev.data )
|
{
|
printk(KERN_ERR " %s driver kmalloc() failed\n", DEV_NAME);
|
return -ENOMEM;
|
}
|
dev.size = BUF_SIZE;
|
dev.bytes = 0;
|
memset(dev.data, 0, dev.size);
|
|
/* allocate device number */
|
if(0 != dev_major)
|
{
|
devno = MKDEV(dev_major, 0);
|
rv = register_chrdev_region(devno, 1, DEV_NAME);
|
}
|
else
|
{
|
rv = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);
|
dev_major = MAJOR(devno);
|
}
|
|
if(rv < 0)
|
{
|
printk(KERN_ERR "%s driver can't use major %d\n", DEV_NAME, dev_major);
|
return -ENODEV;
|
}
|
|
/* initialize cdev and setup fops */
|
cdev_init(&dev.cdev, &chrdev_fops);
|
dev.cdev.owner = THIS_MODULE;
|
|
/* register cdev to linux kernel */
|
rv = cdev_add(&dev.cdev, devno, 1);
|
if( rv )
|
{
|
rv = -ENODEV;
|
printk(KERN_ERR "%s driver regist failed, rv=%d\n", DEV_NAME, rv);
|
goto failed1;
|
}
|
|
/* create device node in user space */
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 5, 0)
|
dev.class = class_create(DEV_NAME);
|
#else
|
dev.class = class_create(THIS_MODULE, DEV_NAME);
|
#endif
|
if (IS_ERR(dev.class)) {
|
rv = PTR_ERR(dev.class);
|
goto failed2;
|
}
|
|
dev.device = device_create(dev.class, NULL, MKDEV(dev_major, 0), NULL, "%s%d", DEV_NAME, 0);
|
if( !dev.device )
|
{
|
rv = -ENODEV;
|
printk(KERN_ERR "%s driver create device failed\n", DEV_NAME);
|
goto failed3;
|
}
|
|
printk(KERN_INFO "%s driver on major[%d] installed.\n", DEV_NAME, dev_major);
|
return 0;
|
|
failed3:
|
class_destroy(dev.class);
|
|
failed2:
|
cdev_del(&dev.cdev);
|
|
failed1:
|
unregister_chrdev_region(devno, 1);
|
kfree(dev.data);
|
|
printk(KERN_ERR "%s driver installed failed.\n", DEV_NAME);
|
return rv;
|
}
|
|
static void __exit chrdev_exit(void)
|
{
|
device_del(dev.device);
|
class_destroy(dev.class);
|
|
cdev_del(&dev.cdev);
|
unregister_chrdev_region(MKDEV(dev_major,0), 1);
|
|
kfree(dev.data);
|
|
printk(KERN_INFO "%s driver removed!\n", DEV_NAME);
|
return;
|
}
|
|
module_init(chrdev_init);
|
module_exit(chrdev_exit);
|
|
MODULE_LICENSE("Dual BSD/GPL");
|
MODULE_AUTHOR("GuoWenxue <guowenxue@gmail.com>");
|