极简字符设备驱动开发:控制继电器、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 上高电平吸合。#includelinux/module.h#includelinux/fs.h#includelinux/cdev.h#includelinux/device.h#includelinux/gpio.h#includelinux/uaccess.h#defineRELAY_GPIO4#defineDEVICE_NAMErelay_ctrl#defineBUF_SIZE8staticdev_tdev_num;staticstructcdevrelay_cdev;staticstructclass*relay_class;staticstructdevice*relay_device;staticcharrelay_state0;// 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(countBUF_SIZE)countBUF_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(countBUF_SIZE)countBUF_SIZE;if(copy_from_user(kbuf,buf,count))return-EFAULT;if(kbuf[0]1){gpio_set_value(RELAY_GPIO,1);relay_state1;pr_info([relay] switched ON\n);}elseif(kbuf[0]0){gpio_set_value(RELAY_GPIO,0);relay_state0;pr_info([relay] switched OFF\n);}returncount;}staticintrelay_release(structinode*inode,structfile*filp){pr_info([relay] device closed\n);return0;}staticconststructfile_operationsrelay_fops{.ownerTHIS_MODULE,.openrelay_open,.readrelay_read,.writerelay_write,.releaserelay_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;}retgpio_request(RELAY_GPIO,relay_gpio);if(ret){pr_err([relay] failed to request GPIO\n);returnret;}gpio_direction_output(RELAY_GPIO,0);/* 默认断开 *//* 2. 动态分配设备号 */retalloc_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.ownerTHIS_MODULE;retcdev_add(relay_cdev,dev_num,1);if(ret){pr_err([relay] cdev_add failed\n);gotoerr_region;}/* 4. 自动创建设备节点 */relay_classclass_create(THIS_MODULE,DEVICE_NAME);relay_devicedevice_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_createdevice_create自动在/dev/下生成设备节点省去手动mknod的麻烦退出时gpio_set_value(RELAY_GPIO, 0)卸载驱动时确保继电器断开防止危险五、实战DI 数字输入与 DO 数字输出DIDigital Input和 DODigital Output本质上也是操作 GPIO只是方向不同/* DI 驱动 - 读取输入电平 */staticssize_tdi_read(structfile*filp,char__user*buf,size_tcount,loff_t*off){charvalgpio_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 Makefileobj-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) clean7.2 编译加载流程# 编译make# 加载驱动insmod relay_ctrl.ko# 查看设备号cat/proc/devices|greprelay# 查看设备节点ls-l/dev/relay_ctrl# 卸载驱动rmmod relay_ctrl如果驱动有依赖关系用modprobe代替insmod它会自动解决依赖。八、用户层测试程序#includestdio.h#includefcntl.h#includeunistd.h#includestring.hintmain(intargc,char*argv[]){intfd;charbuf[8]{0};fdopen(/dev/relay_ctrl,O_RDWR);if(fd0){perror(open failed);return-1;}if(argc1strcmp(argv[1],on)0){write(fd,1,1);printf(Relay ON\n);}elseif(argc1strcmp(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 -f9.2 /proc/devices —— 查看已注册设备cat/proc/devices输出里能看到你的驱动名和主设备号确认是否注册成功。9.3 lsmod —— 查看已加载模块lsmod|greprelay9.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 而已。

相关新闻

openEuler基于certbot申请内网ssl泛域名免费证书

openEuler基于certbot申请内网ssl泛域名免费证书

一、安装 Certbot pip3 install certbot certbot-nginx二、申请证书 单域名申请: certbot certonly --manual --preferred-challenges dns -d jk.xxx.cn泛域名申请: certbot certonly --manual --preferred-challenges dns -d *.xxx.cn -d xxx.cn命令详解…

2026/9/29 8:29:15 阅读更多 →
Skill 安全威胁

Skill 安全威胁

🔴 最高风险(必须防范)威胁攻击方式后果Prompt 注入SKILL.md 中隐藏恶意指令,让 Agent 执行非预期操作被操控、数据泄露、权限滥用数据外传Skill 代码读取文件/内存后发送到外部服务器隐私数据(MEMORY.md、对话历史&am…

2026/10/2 8:26:15 阅读更多 →
老字号数字化营销破圈:江苏老字号博览会全链路传播策略解析

老字号数字化营销破圈:江苏老字号博览会全链路传播策略解析

老字号如何在新消费时代破圈?这是很多传统品牌面临的共同难题。第八届江苏老字号博览会给出了一个值得关注的答案——通过数字化营销和内容创新,让百年老店焕发新生。 作为本次博览会的数字营销合作伙伴,因胜传媒采用了一套系统化的传播策略…

2026/10/12 0:04:36 阅读更多 →

最新新闻

微波通信原理与工程实践:从链路预算到天线安装

微波通信原理与工程实践:从链路预算到天线安装

提到微波通信,很多刚入行或者正在学通信的朋友第一反应是:这东西是不是太老了?光纤都铺进了村里,5G基站密密麻麻,谁还用微波?说实话,我做了十来年通信工程,微波不但没被淘汰&#xf…

2026/10/12 10:54:02 阅读更多 →
AI芯片落地第三阶段:软硬协同系统级调优实战

AI芯片落地第三阶段:软硬协同系统级调优实战

1. 项目概述:这不是芯片设计教科书,而是一份“能跑通、能调优、能量产”的实战手记“AI 芯片的软硬件设计 3”这个标题乍看像课程编号,但在我过去十年参与多个边缘端AI加速器项目的过程中,它实际指向一个非常具体的临界点——当第…

2026/10/12 10:54:02 阅读更多 →
基于半不变量的IEEE 34节点配电网概率潮流计算与Matlab实现

基于半不变量的IEEE 34节点配电网概率潮流计算与Matlab实现

配电网里待久了就会发现,同一个节点不同时刻测到的电压根本不是一个固定值:白天屋顶光伏大发、负荷轻,电压往上飘;晚上负荷重、风场出力不足,电压又往下掉。传统确定性潮流在给定一组负荷和出力后给出一个确定解&#…

2026/10/12 10:54:02 阅读更多 →
AI协同开发STM32:嵌入式工程师的新型工作流

AI协同开发STM32:嵌入式工程师的新型工作流

1. 项目概述:当AI真正坐进嵌入式开发者的工位“AI协同开发STM32程序”——这八个字不是PPT里的概念包装,而是我过去三个月在某工业传感器固件迭代项目中每天真实发生的协作现场。它不等于“用ChatGPT写几行代码”,也不是把Copilot拖进Keil里点…

2026/10/12 10:54:02 阅读更多 →
OpenClaw 模型配置与切换经验分享:把 settings 改到 TaoToken 的实操记录

OpenClaw 模型配置与切换经验分享:把 settings 改到 TaoToken 的实操记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 10:54:02 阅读更多 →
Catalyst 9800无线控制器配置与排错实战指南

Catalyst 9800无线控制器配置与排错实战指南

简介:Catalyst 9800-40无线控制器是思科新一代无线网络管理平台,这一实战配置手册主要面向网络工程师、无线网络运维人员,帮助读者快速上手C9800系列控制器的部署与管理。手册围绕硬件规格、接口说明、配置逻辑、初始配置流程等核心部分展开&…

2026/10/12 10:53:00 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →