news 2026/7/25 2:52:25

极简字符设备驱动开发:控制继电器、DI/DO 数字输入输出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
极简字符设备驱动开发:控制继电器、DI/DO 数字输入输出

极简字符设备驱动开发:控制继电器、DI/DO 数字输入输出

驱动开发不是玄学,今天我们就把"点亮一个继电器"这件事从内核层到用户层扒个底朝天。

一、Linux 驱动模型简介

Linux 设备驱动分三大类,搞不清楚这个分类,后面学起来就容易串台:

设备类型特点典型例子
字符设备按字节流访问,顺序读写串口、GPIO、继电器
块设备按块(扇区)访问,可随机读写硬盘、U盘、SD卡
网络设备面向数据包收发,无设备节点网卡、WiFi

工控场景中,继电器控制、DI/DO 信号采集,本质上都是操作 GPIO 电平,属于典型的字符设备

二、字符设备驱动的核心结构

写字符设备驱动,绕不开三个核心:

2.1 file_operations —— 操作集合

这是驱动和用户空间之间的"接口合同",用户层的open/read/write/close最终都映射到这个结构体里的函数指针:

structfile_operations{int(*open)(structinode*,structfile*);ssize_t(*read)(structfile*,char__user*,size_t,loff_t*);ssize_t(*write)(structfile*,constchar__user*,size_t,loff_t*);int(*release)(structinode*,structfile*);};

2.2 cdev —— 字符设备描述符

struct cdev是内核内部表示字符设备的结构体,需要初始化后添加到内核才能被使用。

2.3 设备号分配

每个设备有一个主设备号(标识驱动)和次设备号(标识同类设备中的具体实例)。分配方式有两种:

  • 静态分配register_chrdev_region(),你需要提前知道哪个号没被占用
  • 动态分配alloc_chrdev_region(),内核自动给你分,推荐用这个

三、极简驱动开发流程

整个开发流程可以浓缩为一句话:模块初始化 → 注册设备 → 实现 fops → 模块卸载

insmod加载 → module_init() → 申请设备号 → 初始化cdev → 添加cdev → 创建设备节点 ↓ 用户程序 → open() → read()/write() → close() ↓ rmmod卸载 → module_exit() → 删除cdev → 释放设备号 → 删除设备节点

四、实战:GPIO 控制继电器驱动

下面给一个完整的、可编译运行的极简继电器控制驱动。继电器接在 GPIO4 上,高电平吸合。

#include<linux/module.h>#include<linux/fs.h>#include<linux/cdev.h>#include<linux/device.h>#include<linux/gpio.h>#include<linux/uaccess.h>#defineRELAY_GPIO4#defineDEVICE_NAME"relay_ctrl"#defineBUF_SIZE8staticdev_tdev_num;staticstructcdevrelay_cdev;staticstructclass*relay_class;staticstructdevice*relay_device;staticcharrelay_state='0';// '0'=断开 '1'=吸合staticintrelay_open(structinode*inode,structfile*filp){pr_info("[relay] device opened\n");return0;}staticssize_trelay_read(structfile*filp,char__user*buf,size_tcount,loff_t*off){if(count>BUF_SIZE)count=BUF_SIZE;if(copy_to_user(buf,&relay_state,1))return-EFAULT;return1;}staticssize_trelay_write(structfile*filp,constchar__user*buf,size_tcount,loff_t*off){charkbuf[BUF_SIZE]={0};if(count>BUF_SIZE)count=BUF_SIZE;if(copy_from_user(kbuf,buf,count))return-EFAULT;if(kbuf[0]=='1'){gpio_set_value(RELAY_GPIO,1);relay_state='1';pr_info("[relay] switched ON\n");}elseif(kbuf[0]=='0'){gpio_set_value(RELAY_GPIO,0);relay_state='0';pr_info("[relay] switched OFF\n");}returncount;}staticintrelay_release(structinode*inode,structfile*filp){pr_info("[relay] device closed\n");return0;}staticconststructfile_operationsrelay_fops={.owner=THIS_MODULE,.open=relay_open,.read=relay_read,.write=relay_write,.release=relay_release,};staticint__initrelay_init(void){intret;/* 1. 申请 GPIO */if(!gpio_is_valid(RELAY_GPIO)){pr_err("[relay] invalid GPIO %d\n",RELAY_GPIO);return-ENODEV;}ret=gpio_request(RELAY_GPIO,"relay_gpio");if(ret){pr_err("[relay] failed to request GPIO\n");returnret;}gpio_direction_output(RELAY_GPIO,0);/* 默认断开 *//* 2. 动态分配设备号 */ret=alloc_chrdev_region(&dev_num,0,1,DEVICE_NAME);if(ret){pr_err("[relay] alloc_chrdev_region failed\n");gotoerr_gpio;}/* 3. 初始化并添加 cdev */cdev_init(&relay_cdev,&relay_fops);relay_cdev.owner=THIS_MODULE;ret=cdev_add(&relay_cdev,dev_num,1);if(ret){pr_err("[relay] cdev_add failed\n");gotoerr_region;}/* 4. 自动创建设备节点 */relay_class=class_create(THIS_MODULE,DEVICE_NAME);relay_device=device_create(relay_class,NULL,dev_num,NULL,DEVICE_NAME);pr_info("[relay] driver loaded, major=%d minor=%d\n",MAJOR(dev_num),MINOR(dev_num));return0;err_region:unregister_chrdev_region(dev_num,1);err_gpio:gpio_free(RELAY_GPIO);returnret;}staticvoid__exitrelay_exit(void){gpio_set_value(RELAY_GPIO,0);/* 退出时断开继电器 */device_destroy(relay_class,dev_num);class_destroy(relay_class);cdev_del(&relay_cdev);unregister_chrdev_region(dev_num,1);gpio_free(RELAY_GPIO);pr_info("[relay] driver unloaded\n");}module_init(relay_init);module_exit(relay_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("黒漂技术佬");MODULE_DESCRIPTION("Simple Relay Control Driver");

4.1 关键点解读

  • copy_to_user/copy_from_user:内核空间和用户空间不能直接互访,必须用这对函数搬运数据,这是安全红线
  • class_create+device_create:自动在/dev/下生成设备节点,省去手动mknod的麻烦
  • 退出时gpio_set_value(RELAY_GPIO, 0):卸载驱动时确保继电器断开,防止危险

五、实战:DI 数字输入与 DO 数字输出

DI(Digital Input)和 DO(Digital Output)本质上也是操作 GPIO,只是方向不同:

/* DI 驱动 - 读取输入电平 */staticssize_tdi_read(structfile*filp,char__user*buf,size_tcount,loff_t*off){charval=gpio_get_value(DI_GPIO)?'1':'0';if(copy_to_user(buf,&val,1))return-EFAULT;return1;}/* DO 驱动 - 设置输出电平 */staticssize_tdo_write(structfile*filp,constchar__user*buf,size_tcount,loff_t*off){charkbuf[4]={0};if(copy_from_user(kbuf,buf,min(count,(size_t)4)))return-EFAULT;gpio_set_value(DO_GPIO,kbuf[0]=='1'?1:0);returncount;}

DI 驱动初始化时用gpio_direction_input(),DO 驱动用gpio_direction_output(),其余框架和继电器驱动一模一样。

六、设备节点创建

两种方式:

方式命令适用场景
手动 mknodmknod /dev/relay_ctrl c 240 0调试阶段,快速验证
自动创建class_create()+device_create()正式发布,推荐

手动mknod需要知道主设备号,可通过cat /proc/devices查看。

七、驱动编译与加载

7.1 Makefile

obj-m := relay_ctrl.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) clean

7.2 编译加载流程

# 编译make# 加载驱动insmod relay_ctrl.ko# 查看设备号cat/proc/devices|greprelay# 查看设备节点ls-l/dev/relay_ctrl# 卸载驱动rmmod relay_ctrl

如果驱动有依赖关系,用modprobe代替insmod,它会自动解决依赖。

八、用户层测试程序

#include<stdio.h>#include<fcntl.h>#include<unistd.h>#include<string.h>intmain(intargc,char*argv[]){intfd;charbuf[8]={0};fd=open("/dev/relay_ctrl",O_RDWR);if(fd<0){perror("open failed");return-1;}if(argc>1&&strcmp(argv[1],"on")==0){write(fd,"1",1);printf("Relay ON\n");}elseif(argc>1&&strcmp(argv[1],"off")==0){write(fd,"0",1);printf("Relay OFF\n");}else{read(fd,buf,1);printf("Relay state: %c\n",buf[0]);}close(fd);return0;}

编译运行:

arm-linux-gnueabihf-gcc test_relay.c-otest_relay# 传到板子上执行./test_relay on# 吸合继电器./test_relay off# 断开继电器./test_relay# 读取当前状态

九、驱动调试技巧

9.1 dmesg —— 查看内核日志

驱动里的pr_info/pr_err输出都走内核日志,用dmesg查看:

dmesg|tail-20# 看最后20条dmesg|greprelay# 过滤关键字dmesg-w# 实时跟踪(类似 tail -f)

9.2 /proc/devices —— 查看已注册设备

cat/proc/devices

输出里能看到你的驱动名和主设备号,确认是否注册成功。

9.3 lsmod —— 查看已加载模块

lsmod|greprelay

9.4 常见问题排查

现象可能原因解决方案
insmod 报错 Invalid module format内核版本不匹配用目标板同版本内核编译
/dev/ 下没有设备节点class_create 失败检查 dmesg 日志
open 报 No such device设备号没注册成功检查 init 函数返回值
GPIO 申请失败GPIO 被其他驱动占用检查设备树 GPIO 占用情况
read/write 返回 -1copy_to/from_user 失败检查用户空间缓冲区大小

字符设备驱动是 Linux 工控开发的根基,把继电器、DI、DO 这套跑通之后,后续的 ADC、PWM、I2C 驱动框架都是同一套路——换一套file_operations,底层换成对应的子系统 API 而已。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/25 2:50:38

Python装饰器的性能代价:从函数调用开销到JIT兼容性分析

Python装饰器的性能代价&#xff1a;从函数调用开销到JIT兼容性分析Python装饰器是实现横切关注点&#xff08;日志、计时、缓存、权限检查&#xff09;的标准手段&#xff0c;但它们引入的额外函数调用层次会带来可测量的性能开销。本文从Python函数调用的底层机制出发&#x…

作者头像 李华
网站建设 2026/7/25 2:49:41

用AI做治愈系音乐号

无意见刷到这个治愈系音乐号,一段热门歌曲片段配上佛系的画风,瞬间感受到慢下来的治愈感,现在生活太累了,很多人刷视频是为了缓口气,解压的。这个账号暖黄色调,治愈氛围,跟着音乐和画面感觉心都慢了。 作品基本只有20秒左右,四五个分镜画面就够,主要是根据歌词设计分镜…

作者头像 李华
网站建设 2026/7/25 2:48:11

计算机毕业设计之基于微信小程序的超市购物平台

随着我国经济迅速发展&#xff0c;人们对小程序的需求越来越大&#xff0c;各种微信小程序也都在被广泛应用&#xff0c;但是对于小程序进行数据信息管理&#xff0c;对于手机的各种小程序也是备受用户的喜爱&#xff0c;小程序超市购物平台被用户普遍使用&#xff0c;为方便用…

作者头像 李华
网站建设 2026/7/25 2:44:18

OCR技术如何提升企业文档处理效率

1. 为什么OCR技术正在重塑现代办公场景每次看到同事对着纸质文件逐字敲键盘录入&#xff0c;我都忍不住想推荐OCR技术。上周市场部的小王为了录入200页产品手册&#xff0c;连续加班三天&#xff0c;最后还因为手误导致数据错乱。这种场景在今天的办公环境中本不该存在——光学…

作者头像 李华
网站建设 2026/7/25 2:43:24

AI时代职场生存指南:从焦虑到掌控的实战策略

1. AI时代普通人的真实处境上周和做设计的朋友吃饭&#xff0c;她突然放下筷子问我&#xff1a;"现在AI一天能出100张海报&#xff0c;我们这些学了十几年美术的人是不是该转行了&#xff1f;"这个问题让我想起最近三个月收到的27条类似咨询——从文案、翻译到程序员…

作者头像 李华
网站建设 2026/7/25 2:43:12

DouyinLiveRecorder抖音直播录制终极指南:轻松捕获40+平台直播内容

DouyinLiveRecorder抖音直播录制终极指南&#xff1a;轻松捕获40平台直播内容 【免费下载链接】DouyinLiveRecorder 可循环值守和多人录制的直播录制软件&#xff0c;支持抖音、TikTok、Youtube、快手、虎牙、斗鱼、B站、小红书、pandatv、sooplive、flextv、popkontv、twitcas…

作者头像 李华