从零编写Linux字符设备驱动:实现自定义传感器的用户层直接访问
自定义传感器接在板端寄存器或I2C/SPI上,最简洁的给用户层“直接访问”方式就是写成字符设备:/dev下出节点,app用open/read拿数据,用ioctl切通道、设采样率。相比sysfs只适合小参数,字符设备可带内核缓冲、阻塞等待、批量帧和高频mmap,更适合工业采样。
一、驱动框架与设备号
用cdev注册,动态申请主设备号,自动建节点:
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define SENS_MAX 16
struct sens_dev {
struct cdev cdev;
dev_t devno;
struct class *cls;
u8 chan; /* 当前通道 */
u8 en; /* 采样使能 */
u16 raw[SENS_MAX]; /* 各通道最新值 */
wait_queue_head_t wq;
atomic_t dirty;
};
static struct sens_dev *g;
static ssize_t sens_read(struct file *fp, char __user *buf, size_t len, loff_t *off)
{
struct sens_dev *s = fp->private_data;
u16 frame[SENS_MAX+1];
if (len < sizeof(frame)) return -EINVAL;
wait_event_interruptible(s->wq, atomic_read(&s->dirty) || s->en==0);
if (signal_pending(current)) return -ERESTARTSYS;
frame[0] = s->chan;
memcpy(&frame[1], s->raw, sizeof(s->raw));
if (copy_to_user(buf, frame, sizeof(frame))) return -EFAULT;
atomic_set(&s->dirty, 0);
return sizeof(frame);
}
static long sens_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
{
struct sens_dev *s = fp->private_data;
switch (cmd) {
case 0x10: /* 设通道 */ if (arg>=SENS_MAX) return -EINVAL; s->chan=arg; break;
case 0x11: /* 使能/停止 */ s->en=!!arg; break;
default: return -ENOTTY;
}
return 0;
}
static int sens_open(struct inode *in, struct file *fp)
{
fp->private_data = container_of(in->i_cdev, struct sens_dev, cdev);
return 0;
}
static const struct file_operations fops = {
.owner = THIS_MODULE, .open = sens_open,
.read = sens_read, .unlocked_ioctl = sens_ioctl,
};
init里申请号、挂cdev、建class/device:
static int __init sens_init(void)
{
g = kzalloc(sizeof(*g), GFP_KERNEL);
init_waitqueue_head(&g->wq);
atomic_set(&g->dirty, 0);
alloc_chrdev_region(&g->devno, 0, 1, "my_sens");
cdev_init(&g->cdev, &fops);
cdev_add(&g->cdev, g->devno, 1);
g->cls = class_create("my_sens_cls");
device_create(g->cls, NULL, g->devno, NULL, "my_sens");
return 0;
}
static void __exit sens_exit(void)
{
device_destroy(g->cls, g->devno);
class_destroy(g->cls);
cdev_del(&g->cdev);
unregister_chrdev_region(g->devno, 1);
kfree(g);
}
module_init(sens_init); module_exit(sens_exit);
MODULE_LICENSE("GPL");
真实硬件把raw[]更新放在定时器或IRQ里:定时器每周期读ADC/I2C,写完atomic_set(&g->dirty,1); wake_up(&g->wq);。寄存器型传感器用ioremap映射基址,read/write寄存器替代kzalloc变量;I2C传感器用i2c_transfer或在probe走i2c_client,但字符设备接口不变。
二、设备树对接(平台传感器)
若传感器是本地IP或挂在平台总线,dts加节点:
my_sens@40001000 {
compatible = "myco,my-sens";
reg = <0x40001000 0x1000>;
clocks = <&clk_peri>;
status = "okay";
};
驱动用platform_driver,probe里platform_get_resource+devm_ioremap_resource取基址,of_match_table填"myco,my-sens";字符设备部分同上,只是把cdev挂到platform设备。这样硬件地址不写死在C里,换板只改dts。
三、用户层直接读
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/ioctl.h>
int main(void){
int fd=open("/dev/my_sens",O_RDWR);
unsigned short buf[17];
ioctl(fd,0x10,0); /* 选通道0 */
ioctl(fd,0x11,1); /* 开始采样 */
while(1){
read(fd,buf,sizeof(buf));
printf("chan=%u raw0=%u raw1=%u\n",buf[0],buf[1],buf[2]);
usleep(100000);
}
close(fd);
}
read返回“通道号+全通道最新值”,用户态零解析硬件;只关心单通道时驱动可改成只拷贝raw[chan]。
四、高频场景补mmap
每秒万次级不想反复系统调用,可加mmap把内核环形采样区映射到用户空间:
static int sens_mmap(struct file *fp, struct vm_area_struct *vma)
{
unsigned long pfn = virt_to_phys(g->raw) >> PAGE_SHIFT;
return remap_pfn_range(vma, vma->vm_start, pfn,
vma->vm_end-vma->vm_start, vma->vm_page_prot);
}
fops增加.mmap=sens_mmap,用户mmap后直接读共享区,驱动只负责更新raw和写序号。注意raw区要按页对齐、不swap,实时系统配合mlockall效果更好。
五、调试与避坑
加载后cat /proc/devices看主设备号、ls /dev/my_sens确认节点;没有节点先查class_create/device_create返回值。
read卡住查waitqueue:硬件更新没wake_up,或en没置1,app会一直睡。
-





