资讯动态

Linux设备驱动开发:从字符设备到I2C的实战闭环

发布时间:2026/9/13 14:40:26 来源:尧图企业网站定制
1. 这不是写代码是给硬件“翻译”人话很多人第一次听说“Linux设备驱动开发”下意识觉得这是在Linux内核里写个C程序、调几个函数、编译加载就完事了。我带过十几届嵌入式方向的实习生八成人在动手前都抱着这种想法——结果三天后坐在工位上盯着dmesg里一串红色的Unknown symbol in module发呆连模块为什么加载失败都说不清。其实设备驱动的本质根本不是编程而是翻译把硬件能听懂的电信号时序比如I2C起始条件、SPI片选拉低、GPIO高低电平持续时间翻译成Linux内核能理解的抽象模型字符设备、块设备、网络设备、平台设备再把用户空间发来的read()、write()、ioctl()这些“人话”精准转译成硬件寄存器的读写序列。这个过程里你既是硬件工程师又是内核架构师还是用户接口设计师——三重身份缺一不可。举个最典型的例子一块温湿度传感器如SHT30通过I2C总线连接到主控。硬件手册告诉你要读温度得先发一个0x2C指令等16ms再从两个寄存器地址连续读2字节。但Linux内核不关心0x2C是什么它只认struct file_operations里的.read函数指针。你的驱动要做的就是在这两者之间架一座桥当应用层执行cat /sys/class/hwmon/hwmon0/device/temp1_input时驱动必须自动完成I2C通信全过程并把原始数据按千分之一摄氏度格式返回。这中间没有魔法只有对硬件行为的精确建模和对内核框架的深度理解。所以别再问“驱动开发难在哪”。难点从来不在语法——C语言就那些关键字难点在于你是否真正看懂了芯片手册里那个时序图里每一条线的上升沿、下降沿、建立时间、保持时间是否理解request_irq()注册中断时IRQF_TRIGGER_LOW和IRQF_TRIGGER_HIGH背后对应的是硬件引脚的物理电平特性是否明白__iomem修饰符强制类型转换的底层意义是告诉编译器“这块内存不能被CPU缓存每次访问都必须直通总线”。关键词里反复出现的“字符设备驱动框架”“设备树配置”“i2c设备驱动详解”说的全是这座桥的不同构件。今天这篇我们就从零开始亲手搭一座能跑通、能调试、能进产线的真实桥梁——不讲虚的只拆解你明天上班就要面对的每一个螺丝钉。2. 字符设备驱动从hello_world到可调试的完整闭环绝大多数初学者的驱动开发之旅始于一个叫hello_world.c的模块。它能insmod能rmmod能在dmesg里打印一行字。但这只是内核模块的“Hello World”离真正的设备驱动差着十万八千里。真正的起点应该是能被用户空间open()、read()、write()的字符设备。我们以一个最简化的LED控制驱动为例走完从注册到验证的全链路。2.1 设备号分配静态 vs 动态为什么必须用动态Linux中每个字符设备都需要一个唯一的主设备号major number和次设备号minor number。早期教材喜欢教静态分配#define MYDEV_MAJOR 240 register_chrdev(MYDEV_MAJOR, myled, fops);这看似简单但实际项目中绝对禁止。原因有三第一主设备号是全局资源240可能已被其他驱动占用比如某些USB摄像头驱动就常用240第二内核版本升级后预留号段会变硬编码极易冲突第三也是最关键的一点——它完全违背了Linux设备模型的动态管理哲学。正确做法是使用alloc_chrdev_region()动态申请dev_t devno; int ret; ret alloc_chrdev_region(devno, 0, 1, myled); // 申请1个次设备号起始为0 if (ret 0) { printk(KERN_ERR Failed to allocate major number\n); return ret; } printk(KERN_INFO Allocated major %d\n, MAJOR(devno));这里MAJOR(devno)会输出实际分配到的主设备号比如241而MINOR(devno)是0。这个号段由内核统一管理绝无冲突。实测中我曾在一个客户项目里因静态分配235号导致与系统自带的uvcvideo驱动冲突摄像头无法识别排查了两天才发现是设备号硬编码惹的祸。提示动态分配后必须用unregister_chrdev_region()配对释放否则卸载模块时内核会报WARNING: at fs/char_dev.c:489且该号段将永久被占用重启都无法恢复。2.2cdev初始化为什么cdev_init()之后还要cdev_add()很多新手把cdev_init()和cdev_add()当成一个操作的两步甚至有人直接删掉cdev_add()结果mknod创建设备节点后open()直接返回-ENXIO。根源在于二者分工明确cdev_init()只做内部结构体初始化把struct cdev的ops字段指向你的file_operations设置owner为THIS_MODULE防止模块被意外卸载cdev_add()才是真正的“注册”动作它把cdev挂入内核的字符设备哈希表让内核知道“当用户访问/dev/myled时该调用谁的.open函数”。关键参数是cdev_add()的第三个参数count它必须与alloc_chrdev_region()申请的设备数量一致。若申请了1个设备号alloc_chrdev_region(..., 0, 1, ...)这里就必须传1若申请了8个..., 0, 8, ...这里就得传8。我见过最离谱的错误是申请了1个号却传8结果cdev_add()成功但只有第一个设备节点可用其余7个open()均失败日志里却没有任何报错——因为内核认为你“主动”注册了8个只是后7个ops为空而已。2.3file_operations实现.open里到底该做什么file_operations是驱动与VFS虚拟文件系统的契约。其中.open常被误写成空函数或只打印日志。但真实场景中它必须完成硬件初始化static int myled_open(struct inode *inode, struct file *file) { // 1. 获取设备私有数据见2.4节 struct myled_dev *dev container_of(inode-i_cdev, struct myled_dev, cdev); // 2. 申请并配置GPIO假设LED接在GPIO12 if (!gpio_is_valid(12)) { return -ENODEV; } if (gpio_request_one(12, GPIOF_OUT_INIT_LOW, myled)) { return -EBUSY; } // 3. 设置GPIO方向为输出初始状态为低电平LED灭 gpio_direction_output(12, 0); // 4. 将私有数据存入file-private_data供后续read/write使用 file-private_data dev; return 0; }这里gpio_request_one()是关键。它不仅申请GPIO资源还做了三件事检查该GPIO是否已被其他驱动占用避免硬件冲突根据GPIOF_OUT_INIT_LOW参数自动配置为输出模式并设初值在/sys/class/gpio/下创建对应目录方便用户空间调试。注意gpio_request_one()必须在.open里调用而非模块初始化时。因为设备节点可被多次open()但GPIO资源只能由一个进程独占。若在init里申请第二个open()就会失败。这也是为什么.release里必须调用gpio_free(12)——资源归还。2.4 私有数据管理container_of()不是炫技是生存必需驱动中必然存在设备私有数据如LED的GPIO号、I2C适配器指针、寄存器基地址等。内核不提供全局变量存储必须通过file-private_data传递。而container_of()是获取该数据的唯一安全方式struct myled_dev { struct cdev cdev; int gpio_num; struct device *dev; }; static int myled_open(struct inode *inode, struct file *file) { // 关键从inode-i_cdev反推struct myled_dev的起始地址 struct myled_dev *dev container_of(inode-i_cdev, struct myled_dev, cdev); file-private_data dev; return 0; } static ssize_t myled_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { struct myled_dev *dev file-private_data; // 直接使用 char kbuf[16]; if (count sizeof(kbuf) - 1) return -EINVAL; if (copy_from_user(kbuf, buf, count)) return -EFAULT; kbuf[count] \0; if (strncmp(kbuf, on, 2) 0) { gpio_set_value(dev-gpio_num, 1); // 点亮 } else if (strncmp(kbuf, off, 3) 0) { gpio_set_value(dev-gpio_num, 0); // 熄灭 } return count; }container_of(ptr, type, member)的原理是已知结构体中某个成员member的地址ptr求整个结构体type的首地址。它通过offsetof(type, member)计算偏移量实现。不用它而用强制类型转换如(struct myled_dev*)inode-i_cdev是危险的——因为i_cdev是struct cdev类型指针而struct myled_dev包含cdev作为成员二者地址不同。我曾在一个工业网关项目里因手写强制转换导致dev-gpio_num读取到随机内存LED状态完全失控花了半天才定位到这个低级错误。3. 设备树硬件描述的“宪法”不是可选项当你的驱动要适配不同硬件板卡比如同一款SoC有的板子LED接GPIO12有的接GPIO45硬编码GPIO号就彻底失效了。Linux解决方案是设备树Device Tree它把硬件配置从内核代码中剥离变成独立的.dts文件。这不是锦上添花而是现代驱动开发的基石。3.1 设备树节点定义compatible字符串是驱动的“身份证”在板级设备树文件如arch/arm/boot/dts/myboard.dts中为LED添加节点soc { myled: led0 { compatible mycompany,led; reg 0x0; // 次设备号用于多LED区分 gpios gpio1 12 GPIO_ACTIVE_HIGH; // GPIO1组第12号高电平有效 status okay; }; };关键在compatible mycompany,led。内核在匹配驱动时会遍历所有设备树节点查找其compatible字符串是否与驱动中of_match_table定义的字符串一致。驱动端代码如下static const struct of_device_id myled_of_match[] { { .compatible mycompany,led }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myled_of_match); static struct platform_driver myled_driver { .probe myled_probe, .remove myled_remove, .driver { .name myled, .of_match_table myled_of_match, }, };MODULE_DEVICE_TABLE(of, ...)宏至关重要——它告诉内核构建系统“这个驱动支持设备树匹配”否则即使compatible写对了内核也不会尝试绑定。我调试过一个客户项目驱动一直无法probe最后发现是忘了加这行宏dmesg里连匹配尝试的日志都没有。3.2platform_driver探针函数从设备树提取资源的完整流程probe函数是设备树驱动的核心入口。它必须完成三件事解析设备树属性、申请硬件资源、初始化设备。以下是标准模板static int myled_probe(struct platform_device *pdev) { struct myled_dev *dev; struct device_node *np pdev-dev.of_node; int ret, gpio; // 1. 分配设备私有结构体 dev devm_kzalloc(pdev-dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; // 2. 从设备树获取GPIO号 ret of_get_gpio(np, gpios, 0); // 索引0取第一个gpios属性 if (ret 0) { dev_err(pdev-dev, Failed to get gpio from dt\n); return ret; } gpio ret; // 3. 验证GPIO有效性并申请 if (!gpio_is_valid(gpio)) { dev_err(pdev-dev, Invalid gpio %d\n, gpio); return -ENODEV; } ret devm_gpio_request_one(pdev-dev, gpio, GPIOF_OUT_INIT_LOW, myled); if (ret) { dev_err(pdev-dev, Failed to request gpio %d\n, gpio); return ret; } // 4. 保存私有数据到platform_device platform_set_drvdata(pdev, dev); dev-gpio_num gpio; // 5. 注册字符设备复用2.1节逻辑 ret alloc_chrdev_region(dev-devno, 0, 1, myled); if (ret 0) goto err_alloc; cdev_init(dev-cdev, myled_fops); dev-cdev.owner THIS_MODULE; ret cdev_add(dev-cdev, dev-devno, 1); if (ret 0) goto err_cdev; // 6. 创建设备节点自动绑定到/sys/class/... dev-class class_create(THIS_MODULE, myled); if (IS_ERR(dev-class)) { ret PTR_ERR(dev-class); goto err_class; } dev-device device_create(dev-class, NULL, dev-devno, NULL, myled); if (IS_ERR(dev-device)) { ret PTR_ERR(dev-device); goto err_device; } dev_info(pdev-dev, LED driver probed successfully\n); return 0; err_device: class_destroy(dev-class); err_class: cdev_del(dev-cdev); err_cdev: unregister_chrdev_region(dev-devno, 1); err_alloc: return ret; }这里devm_*系列函数devm_kzalloc,devm_gpio_request_one是重点。devm_前缀表示“设备管理”内核会自动在platform_device销毁时如模块卸载或设备热拔插释放这些资源无需手动清理。这极大降低了内存泄漏风险。我维护过一个老项目驱动里全是kmallockfree结果一次热拔插后kmemleak检测出2MB内存未释放追查发现是remove函数漏写了kfree。3.3 设备树覆盖如何不改源码就能适配新硬件客户送来一块新板子LED换到了GPIO55。你不需要修改驱动代码只需提供一个新的设备树覆盖overlay文件myboard-led-overlay.dts/dts-v1/; /plugin/; / { compatible mycompany,myboard; fragment0 { target soc; __overlay__ { myled: led0 { gpios gpio1 55 GPIO_ACTIVE_HIGH; }; }; }; };编译为myboard-led-overlay.dtbo在启动时通过fdt_overlays参数加载。内核会自动将此覆盖合并到主设备树中of_get_gpio()读取到的就是55。这种“硬件配置即代码”的思想让驱动真正实现了“一次编写多板运行”。某汽车电子客户要求同一套驱动适配5种ECU硬件靠的就是这套设备树覆盖机制交付周期缩短了70%。4. I2C设备驱动从总线控制器到从设备的全栈解析字符设备驱动解决的是“单点控制”而I2C驱动解决的是“总线通信”。网上搜索热词里高频出现的“i2c设备驱动详解”“i2c设备驱动的注册函数”恰恰说明这是驱动开发中最易踩坑的领域。我们以SHT30温湿度传感器为例拆解I2C驱动的完整链条。4.1 I2C总线控制器驱动硬件适配层的“地基”I2C通信需要两层驱动控制器驱动Controller Driver适配SoC内置的I2C IP核如Exynos的HS-I2C、Xilinx Zynq的AXI I2C负责产生SCL/SDA时序、处理中断、DMA传输从设备驱动Client Driver适配具体传感器如SHT30负责发送命令、解析数据。新手常混淆二者。控制器驱动由SoC厂商提供位于drivers/i2c/busses/用户通常只需在设备树中配置其参数。例如ZynqMP的I2C0控制器i2c0 { clock-frequency 100000; // 标准模式100kHz #address-cells 1; #size-cells 0; status okay; sht3044 { compatible sensirion,sht30; reg 0x44; // SHT30的7位I2C地址 vcc-supply vcc_3v3; }; };clock-frequency设置总线速率reg指定从设备地址。这里compatible sensirion,sht30会触发内核自动加载drivers/i2c/i2c-core-smbus.c中的通用SHT30驱动如果已启用或你的自定义驱动。4.2 I2C从设备驱动注册i2c_register_driver()的隐含契约从设备驱动需调用i2c_register_driver()注册其核心是i2c_device_id表static const struct i2c_device_id sht30_id[] { { sht30, 0 }, // 名称必须与设备树compatible或模块别名一致 { } }; MODULE_DEVICE_TABLE(i2c, sht30_id); static struct i2c_driver sht30_driver { .driver { .name sht30, .of_match_table of_match_ptr(sht30_of_match), }, .probe sht30_probe, .id_table sht30_id, // 关键必须提供 }; module_i2c_driver(sht30_driver);id_table的作用常被忽视它告诉内核“本驱动支持哪些设备ID”。当设备树节点compatible sensirion,sht30与id_table中sht30匹配时内核才会调用probe。若遗漏id_table即使compatible写对probe也永远不会执行。我在Xilinx Zynq平台上调试SHT30时就因忘记加id_tabledmesg里只显示i2c i2c-0: Failed to register driver sht30毫无头绪最终逐行对比内核文档才发现这个硬性要求。4.3probe函数中的I2C通信i2c_transfer()与i2c_smbus_read_word_data()的选择SHT30读取温度需发送2字节命令0x2C 0x06等待16ms再读取2字节数据。驱动中两种实现方式方式一原始i2c_transfer()推荐static int sht30_read_temp(struct sht30_data *data, u16 *val) { struct i2c_client *client >// 仅适用于标准SMBus命令如读取字节、字、块 ret i2c_smbus_read_word_data(client, 0x2C); // 错误0x2C是命令非寄存器地址i2c_smbus_*函数专为SMBus协议设计要求从设备支持标准寄存器寻址。而SHT30是“命令型”设备没有寄存器概念i2c_smbus_read_word_data()会发送[addr][0x2C][addr]序列导致通信失败。必须用i2c_transfer()手动构造消息。我曾因误用i2c_smbusdmesg里看到i2c i2c-0: sendbytes: NAK bailout折腾半天才意识到协议不匹配。4.4 数据校验与错误处理CRC不是摆设SHT30每2字节数据后跟1字节CRC校验码。忽略CRC会导致读数漂移。校验函数必须严格按手册实现static u8 sht30_crc8(u8 *data, int len) { u8 crc 0xFF; int i, j; for (i 0; i len; i) { crc ^ data[i]; for (j 0; j 8; j) { if (crc 0x80) crc (crc 1) ^ 0x31; else crc 1; } } return crc; } // 在read_temp中调用 if (buf[2] ! sht30_crc8(buf, 2)) { // buf[0],buf[1]是数据buf[2]是CRC dev_err(client-dev, CRC check failed\n); return -EIO; }这个CRC算法是SHT30手册明确定义的多项式0x31不能替换成通用CRC8。某次量产测试中因CRC校验逻辑写错一批传感器在高温环境下读数偏差达±5℃返工损失数十万元。教训是驱动中的硬件协议实现必须逐字对照芯片手册任何“大概差不多”都是灾难源头。5. 调试实战从dmesg红字到perf火焰图的全链路追踪写完驱动不等于结束90%的工作量在调试。网上热词里“linux驱动开发入门”“linux面试题”高频出现但真正拉开差距的是调试能力。以下是我十年间总结的调试铁律。5.1dmesg日志读懂内核的“黑匣子”dmesg是第一道防线但新手常犯两个错误只看最后一行忽略上下文不加-T或-H参数时间戳混乱无法关联事件。正确姿势# 实时监控带人类可读时间戳 dmesg -wH # 过滤特定模块如myled dmesg | grep -i myled # 查看最近100行含时间戳 dmesg -n 8 # 设置日志级别确保info及以上可见 dmesg -L --coloralways | tail -100典型日志解读myled: Failed to request gpio 12→ GPIO12被占用用cat /sys/kernel/debug/gpio查看占用者myled: Unknown symbol in module→ 模块依赖未声明检查MODULE_LICENSE(GPL)和MODULE_AUTHOR()是否缺失i2c i2c-0: sendbytes: NAK bailout→ I2C从设备未响应用逻辑分析仪抓波形确认地址、时序、上拉电阻。提示在驱动中用pr_debug()打印调试信息但必须配合dynamic_debug启用echo file myled.c p /sys/kernel/debug/dynamic_debug/control这样比printk(KERN_DEBUG ...)更灵活上线时可一键关闭。5.2sysfs与debugfs用户空间的“驾驶舱”内核提供了标准接口暴露设备状态。sysfs用于配置debugfs用于诊断// 在probe中创建sysfs属性 static ssize_t led_state_show(struct device *dev, struct device_attribute *attr, char *buf) { struct myled_dev *led dev_get_drvdata(dev); int state gpio_get_value(led-gpio_num); return sprintf(buf, %d\n, state); } static ssize_t led_state_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct myled_dev *led dev_get_drvdata(dev); unsigned long state; if (kstrtoul(buf, 0, state)) return -EINVAL; gpio_set_value(led-gpio_num, state ? 1 : 0); return count; } static DEVICE_ATTR_RW(led_state); // 创建debugfs文件 static int debugfs_open(struct inode *inode, struct file *file) { file-private_data inode-i_private; return 0; } static const struct file_operations debugfs_fops { .open debugfs_open, .read debugfs_read, // 自定义读取逻辑如打印寄存器快照 }; // probe中 led-debugfs_dir debugfs_create_dir(myled, NULL); debugfs_create_file(regs_dump, 0444, led-debugfs_dir, led, debugfs_fops);这样用户就能echo 1 /sys/devices/platform/myled/led_state控制LEDcat /sys/devices/platform/myled/led_state查询状态cat /sys/kernel/debug/myled/regs_dump查看硬件寄存器当前值。某次客户现场问题LED在特定温度下闪烁。我远程指导客户执行cat /sys/kernel/debug/myled/regs_dump发现GPIO控制器寄存器中OUTPUT_EN位被意外清零顺藤摸瓜定位到电源管理驱动的bug。这就是debugfs的价值——把内核状态变成可观察、可测量的数据。5.3perf性能分析揪出CPU时间黑洞驱动性能问题如read()延迟高、中断抖动大无法靠日志发现。perf是终极武器# 记录驱动函数的CPU耗时 perf record -e cycles,instructions -g -p $(pidof your_app) # 生成火焰图需安装flamegraph perf script | ~/FlameGraph/stackcollapse-perf.pl | ~/FlameGraph/flamegraph.pl kernel.svg # 专门分析中断处理时间 perf record -e irq:irq_handler_entry,irq:irq_handler_exit -a在一次GPU驱动优化中perf火焰图显示drm_vblank_enable()函数占CPU时间35%远超预期。深入分析发现是垂直同步中断过于频繁通过调整vblank_disable_interval参数帧率提升22%。没有perf这种深层次性能瓶颈根本无法定位。5.4 硬件级调试逻辑分析仪是驱动工程师的“听诊器”所有软件调试手段都有盲区。当dmesg无报错、sysfs显示正常但硬件无响应时必须上硬件工具。我包里永远装着Saleae Logic 8抓I2C波形确认起始/停止条件、地址、ACK/NACK、数据字节测GPIO电平验证gpio_set_value()是否真的改变了引脚状态量中断信号看request_irq()注册的中断是否被硬件触发。曾有一个项目request_irq()返回0dmesg显示“IRQ XX: registered”但中断服务函数irq_handler死活不执行。用逻辑分析仪一测发现硬件设计缺陷中断引脚悬空未接下拉电阻导致电平不稳定。软件再完美也救不了硬件的锅。记住驱动开发的终点永远在示波器的屏幕上。6. 从实验室到产线驱动稳定性的七道生死关写一个能insmod的驱动容易写一个能7×24小时运行在工业现场的驱动极难。热词中“linux嵌入式驱动开发”“系统裁剪优化”暗示了真实战场的需求。以下是经过产线验证的稳定性守则。6.1 电源管理suspend/resume不是可选项工业设备常需休眠唤醒。若驱动不实现pm_ops休眠时硬件状态丢失唤醒后功能异常。标准模板static int myled_suspend(struct device *dev) { struct myled_dev *led dev_get_drvdata(dev); // 保存关键状态当前LED状态、GPIO配置 led-saved_state gpio_get_value(led-gpio_num); return 0; } static int myled_resume(struct device *dev) { struct myled_dev *led dev_get_drvdata(dev); // 恢复状态 gpio_set_value(led-gpio_num, led-saved_state); return 0; } static const struct dev_pm_ops myled_pm_ops { .suspend myled_suspend, .resume myled_resume, }; // 在platform_driver中绑定 static struct platform_driver myled_driver { // ... .driver { .pm myled_pm_ops, }, };某电力监测终端因未实现resume休眠后传感器数据全乱客户投诉为“致命缺陷”。补上pm_ops后通过了IEC 61000-4-2静电放电测试。6.2 并发安全mutex与spinlock的抉择多个应用同时write()同一个LED设备节点open()期间硬件被其他进程抢占必须加锁。但选错锁类型会引发死锁或实时性崩溃mutex睡眠锁用于可能阻塞的操作如copy_from_user、msleep不能在中断上下文使用spinlock忙等锁用于短时间临界区如寄存器读写可在中断上下文使用但持有期间禁止睡眠。static DEFINE_MUTEX(myled_mutex); static ssize_t myled_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { mutex_lock(myled_mutex); // 正确保护用户空间拷贝和GPIO操作 // ... copy_from_user, gpio_set_value ... mutex_unlock(myled_mutex); return count; } // 中断服务函数中 static irqreturn_t myled_irq_handler(int irq, void *dev_id) { struct myled_dev *led dev_id; spin_lock(led-irq_lock); // 正确中断上下文只能用spinlock // ... 更新状态计数器 ... spin_unlock(led-irq_lock); return IRQ_HANDLED; }曾因在中断里用mutex_lock()导致系统死锁dmesg里满屏BUG: scheduling while atomic。教训锁的类型选择是驱动稳定性的第一道门槛。6.3 内存安全__user与__iomem的不可逾越用户空间指针__user和IO内存指针__iomem有严格隔离。跨域访问必崩// 错误直接解引用用户指针 char *ptr buf; // buf是__user char* *ptr a; // 编译警告运行时段错误 // 正确用专用函数 char kbuf[16]; if (copy_from_user(kbuf, buf, min_t(size_t, count, sizeof(kbuf)-1))) return -EFAULT; // 错误直接访问IO内存 u32 *reg (u32*)0x40000000; *reg 0x1; // 可能被CPU缓存硬件无响应 // 正确用ioremap iowrite32 void __iomem *base ioremap(0x40000000, 0x10

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价