————增添字符驱动)
1字符设备驱动整体结构1. PCI 驱动骨架 → module_pci_driver → pci_device_id匹配 1234:11e8 → probe / remove 2. 设备私有数据 struct edu_pci_dev → 保存 pci_dev、bar0 映射地址、misc 设备 3. 字符设备接口/dev/edu0 → open/read/write/ioctl → misc_register 4. 硬件操作封装 → edu_readl / edu_writelioread32/iowrite321.1注册流程说明用miscdevice提供/dev/edu0用ioctl/read/write让用户态操作硬件寄存器1.1.1 PCI驱动骨架1.1.1.1厂商ID设备IDVendor ID Device ID/* QEMU edu 设备固定的 PCI Vendor ID / Device ID。 */ #define EDU_VENDOR_ID 0x1234 #define EDU_DEVICE_ID 0x11e81.1.1.2 结构体标明成员变量有①pci_dev这个说明该驱动是搞这个pci设备对象的②_iomem则是标明这个bar0是要映射到内核的虚拟地址上的③resource_size_t 该变量能够反应 bar 究竟是想要多大内核空间④instance该成员变量的作用是 当前pc要是有不同的edu设备的话 每个设备都会有不同的instance实例。⑤miscdevice:则是内核提供专门供给字符设备注册使用的结构体/* * 每一个 edu PCI function 对应一个 edu_pci_dev。 * * pdevLinux PCI 子系统创建的 PCI 设备对象。 * bar0BAR0 ioremap 后的内核虚拟地址只能用 ioread/iowrite 访问。 * miscdevmisc 字符设备对象注册成功后生成 /dev/eduN。 */ struct edu_pci_dev { struct pci_dev *pdev; void __iomem *bar0; resource_size_t bar0_len; int instance; struct miscdevice miscdev; char misc_name[32]; };1.1.1.3 设定硬件设备匹配表通过该表可实现 同一驱动程序适用不同的硬件设备本质上这个表也是要逐渐上交给pci子系统在子系统内部进行匹配配成了再经由子系统决定之心probe函数/* PCI 匹配表告诉 PCI core 本驱动支持 QEMU edu 设备 1234:11e8。 */ static const struct pci_device_id edu_pci_ids[] { { PCI_DEVICE(EDU_VENDOR_ID, EDU_DEVICE_ID) }, { } }; MODULE_DEVICE_TABLE(pci, edu_pci_ids);安装驱动之后也可以动态的修改new_id 和 remove_id这两个接口允许你动态添加和删除驱动支持的设备 ID不需要重新编译驱动。# 让 edu_pci 驱动支持 Vendor ID 0x1234Device ID 0x11e9 的设备 echo 1234 11e9 new_id # 移除刚才添加的设备 ID echo 1234 11e9 remove_id调试新设备时可以快速测试驱动是否能支持新的设备 ID不需要修改驱动代码和重新编译可以临时让一个通用驱动支持特定的设备1.1.1.3 probe函数主要就是向pci子系统申请一些资源自定义结构体与子系统结构体绑定①pci_devpci_device_idprobe函数的两个入口参数是由pci子系统pci core下发的就是告诉probe函数欸你用户自己写的驱动提交的表格和我pci core枚举的硬件设备匹配中了其pci_dev与pci_device_id我交给你(用户写的probe函数)以下是内核维护的pci_dev结构体include/linux/pci.h, line 322 (as a struct)/* The pci_dev structure describes PCI devices */ struct pci_dev { struct list_head bus_list; /* Node in per-bus list */ struct pci_bus *bus; /* Bus this device is on */ struct pci_bus *subordinate; /* Bus this device bridges to */ void *sysdata; /* Hook for sys-specific extension */ struct proc_dir_entry *procent; /* Device entry in /proc/bus/pci */ struct pci_slot *slot; /* Physical slot this device is in */ unsigned int devfn; /* Encoded device function index */ unsigned short vendor; unsigned short device; unsigned short subsystem_vendor; unsigned short subsystem_device; unsigned int class; /* 3 bytes: (base,sub,prog-if) */ u8 revision; /* PCI revision, low byte of class word */ u8 hdr_type; /* PCI header type (multi flag masked out) */ #ifdef CONFIG_PCIEAER u16 aer_cap; /* AER capability offset */ struct aer_stats *aer_stats; /* AER stats for this device */ #endif #ifdef CONFIG_PCIEPORTBUS struct rcec_ea *rcec_ea; /* RCEC cached endpoint association */ struct pci_dev *rcec; /* Associated RCEC device */ #endif u32 devcap; /* PCIe Device Capabilities */ u8 pcie_cap; /* PCIe capability offset */ u8 msi_cap; /* MSI capability offset */ u8 msix_cap; /* MSI-X capability offset */ u8 pcie_mpss:3; /* PCIe Max Payload Size Supported */ u8 rom_base_reg; /* Config register controlling ROM */ u8 pin; /* Interrupt pin this device uses */ u16 pcie_flags_reg; /* Cached PCIe Capabilities Register */ unsigned long *dma_alias_mask;/* Mask of enabled devfn aliases */ struct pci_driver *driver; /* Driver bound to this device */ u64 dma_mask; /* Mask of the bits of bus address this device implements. Normally this is 0xffffffff. You only need to change this if your device has broken DMA or supports 64-bit transfers. */ struct device_dma_parameters dma_parms; pci_power_t current_state; /* Current operating state. In ACPI, this is D0-D3, D0 being fully functional, and D3 being off. */ u8 pm_cap; /* PM capability offset */ unsigned int imm_ready:1; /* Supports Immediate Readiness */ unsigned int pme_support:5; /* Bitmask of states from which PME# can be generated */ unsigned int pme_poll:1; /* Poll devices PME status bit */ unsigned int d1_support:1; /* Low power state D1 is supported */ unsigned int d2_support:1; /* Low power state D2 is supported */ unsigned int no_d1d2:1; /* D1 and D2 are forbidden */ unsigned int no_d3cold:1; /* D3cold is forbidden */ unsigned int bridge_d3:1; /* Allow D3 for bridge */ unsigned int d3cold_allowed:1; /* D3cold is allowed by user */ unsigned int mmio_always_on:1; /* Disallow turning off io/mem decoding during BAR sizing */ unsigned int wakeup_prepared:1; unsigned int skip_bus_pm:1; /* Internal: Skip bus-level PM */ unsigned int ignore_hotplug:1; /* Ignore hotplug events */ unsigned int hotplug_user_indicators:1; /* SlotCtl indicators controlled exclusively by user sysfs */ unsigned int clear_retrain_link:1; /* Need to clear Retrain Link bit manually */ unsigned int d3hot_delay; /* D3hot-D0 transition time in ms */ unsigned int d3cold_delay; /* D3cold-D0 transition time in ms */ #ifdef CONFIG_PCIEASPM struct pcie_link_state *link_state; /* ASPM link state */ u16 l1ss; /* L1SS Capability pointer */ unsigned int ltr_path:1; /* Latency Tolerance Reporting supported from root to here */ #endif unsigned int pasid_no_tlp:1; /* PASID works without TLP Prefix */ unsigned int eetlp_prefix_path:1; /* End-to-End TLP Prefix */ pci_channel_state_t error_state; /* Current connectivity state */ struct device dev; /* Generic device interface */ int cfg_size; /* Size of config space */ /* * Instead of touching interrupt line and base address registers * directly, use the values stored here. They might be different! */ unsigned int irq; struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions expansion ROMs */ struct resource driver_exclusive_resource; /* driver exclusive resource ranges */ bool match_driver; /* Skip attaching driver */ unsigned int transparent:1; /* Subtractive decode bridge */ unsigned int io_window:1; /* Bridge has I/O window */ unsigned int pref_window:1; /* Bridge has pref mem window */ unsigned int pref_64_window:1; /* Pref mem window is 64-bit */ unsigned int multifunction:1; /* Multi-function device */ unsigned int is_busmaster:1; /* Is busmaster */ unsigned int no_msi:1; /* May not use MSI */ unsigned int no_64bit_msi:1; /* May only use 32-bit MSIs */ unsigned int block_cfg_access:1; /* Config space access blocked */ unsigned int broken_parity_status:1; /* Generates false positive parity */ unsigned int irq_reroute_variant:2; /* Needs IRQ rerouting variant */ unsigned int msi_enabled:1; unsigned int msix_enabled:1; unsigned int ari_enabled:1; /* ARI forwarding */ unsigned int ats_enabled:1; /* Address Translation Svc */ unsigned int pasid_enabled:1; /* Process Address Space ID */ unsigned int pri_enabled:1; /* Page Request Interface */ unsigned int is_managed:1; /* Managed via devres */ unsigned int is_msi_managed:1; /* MSI release via devres installed */ unsigned int needs_freset:1; /* Requires fundamental reset */ unsigned int state_saved:1; unsigned int is_physfn:1; unsigned int is_virtfn:1; unsigned int is_hotplug_bridge:1; unsigned int shpc_managed:1; /* SHPC owned by shpchp */ unsigned int is_thunderbolt:1; /* Thunderbolt controller */ /* * Devices marked being untrusted are the ones that can potentially * execute DMA attacks and similar. They are typically connected * through external ports such as Thunderbolt but not limited to * that. When an IOMMU is enabled they should be getting full * mappings to make sure they cannot access arbitrary memory. */ unsigned int untrusted:1; /* * Info from the platform, e.g., ACPI or device tree, may mark a * device as external-facing. An external-facing device is * itself internal but devices downstream from it are external. */ unsigned int external_facing:1; unsigned int broken_intx_masking:1; /* INTx masking cant be used */ unsigned int io_window_1k:1; /* Intel bridge 1K I/O windows */ unsigned int irq_managed:1; unsigned int non_compliant_bars:1; /* Broken BARs; ignore them */ unsigned int is_probed:1; /* Device probing in progress */ unsigned int link_active_reporting:1;/* Device capable of reporting link active */ unsigned int no_vf_scan:1; /* Dont scan for VFs after IOV enablement */ unsigned int no_command_memory:1; /* No PCI_COMMAND_MEMORY */ unsigned int rom_bar_overlap:1; /* ROM BAR disable broken */ unsigned int rom_attr_enabled:1; /* Display of ROM attribute enabled? */ pci_dev_flags_t dev_flags; atomic_t enable_cnt; /* pci_enable_device has been called */ spinlock_t pcie_cap_lock; /* Protects RMW ops in capability accessors */ u32 saved_config_space[16]; /* Config space saved at suspend time */ struct hlist_head saved_cap_space; struct bin_attribute *res_attr[DEVICE_COUNT_RESOURCE]; /* sysfs file for resources */ struct bin_attribute *res_attr_wc[DEVICE_COUNT_RESOURCE]; /* sysfs file for WC mapping of resources */ #ifdef CONFIG_HOTPLUG_PCI_PCIE unsigned int broken_cmd_compl:1; /* No compl for some cmds */ #endif #ifdef CONFIG_PCIE_PTM u16 ptm_cap; /* PTM Capability */ unsigned int ptm_root:1; unsigned int ptm_enabled:1; u8 ptm_granularity; #endif #ifdef CONFIG_PCI_MSI void __iomem *msix_base; raw_spinlock_t msi_lock; #endif struct pci_vpd vpd; #ifdef CONFIG_PCIE_DPC u16 dpc_cap; unsigned int dpc_rp_extensions:1; u8 dpc_rp_log_size; #endif #ifdef CONFIG_PCI_ATS union { struct pci_sriov *sriov; /* PF: SR-IOV info */ struct pci_dev *physfn; /* VF: related PF */ }; u16 ats_cap; /* ATS Capability offset */ u8 ats_stu; /* ATS Smallest Translation Unit */ #endif #ifdef CONFIG_PCI_PRI u16 pri_cap; /* PRI Capability offset */ u32 pri_reqs_alloc; /* Number of PRI requests allocated */ unsigned int pasid_required:1; /* PRG Response PASID Required */ #endif #ifdef CONFIG_PCI_PASID u16 pasid_cap; /* PASID Capability offset */ u16 pasid_features; #endif #ifdef CONFIG_PCI_P2PDMA struct pci_p2pdma __rcu *p2pdma; #endif #ifdef CONFIG_PCI_DOE struct xarray doe_mbs; /* Data Object Exchange mailboxes */ #endif u16 acs_cap; /* ACS Capability offset */ phys_addr_t rom; /* Physical address if not from BAR */ size_t romlen; /* Length if not from BAR */ /* * Driver name to force a match. Do not set directly, because core * frees it. Use driver_set_override() to set or clear it. */ const char *driver_override; unsigned long priv_flags; /* Private flags for the PCI driver */ /* These methods index pci_reset_fn_methods[] */ u8 reset_methods[PCI_NUM_RESET_METHODS]; /* In priority order */ };②ret pcim_enable_device(pdev);启动匹配中的pci设备以下说明是关于启动的具体描述/** * pci_enable_device - Initialize device before its used by a driver. * dev: PCI device to be initialized * * Initialize device before its used by a driver. Ask low-level code * to enable I/O and memory. Wake up the device if it was suspended. * Beware, this function can fail. * * Note we dont actually enable the device many times if we call * this function repeatedly (we just increment the count). */③ret pcim_iomap_regions(pdev, BIT(EDU_BAR), edu_pci);请求并映射 BAR0就是把这个BAR0映射到内核的虚拟空间里边后续通过 edu-bar0 访问 MMIO 寄存器。/** * pcim_iomap_table - access iomap allocation table * pdev: PCI device to access iomap table for * * Access iomap allocation table for dev. If iomap table doesnt * exist and pdev is managed, it will be allocated. All iomaps * recorded in the iomap table are automatically unmapped on driver * detach. * * This function might sleep when the table is first allocated but can * be safely called without context and guaranteed to succeed once * allocated. */④ edu devm_kzalloc(pdev-dev, sizeof(*edu), GFP_KERNEL);申请一段内存用来存放我们自己定义的私有结构体struct edu_pci_dev这段内存的生命周期 绑定 到 pdev-dev 这个设备上 函数返回新分配结构体的指针保存到edu这部分只是让内核帮我们托管这块内存设备销毁时自动释放。也就是开辟一块空白的内存地址私有结构体edu指向这块空地并且这个空地内存的生命周期和内核维护的pdev-dev 这个设备是一样这样的话当pci core释放这个pdev-dev 时候内存也会被回收。/** * devm_kmalloc - Resource-managed kmalloc * dev: Device to allocate memory for * size: Allocation size * gfp: Allocation gfp flags * * Managed kmalloc. Memory allocated with this function is * automatically freed on driver detach. Like all other devres * resources, guaranteed alignment is unsigned long long. * * RETURNS: * Pointer to allocated memory on success, NULL on failure. */⑤edu-bar0 pcim_iomap_table(pdev)[EDU_BAR];edu-bar0_len pci_resource_len(pdev, EDU_BAR);就是把pci core发现的 匹配的硬件设备的相关信息信息存放在内核给我们用户自定义的驱动部分⑥pci_set_drvdata(pdev, edu);实现用户定义的私有结构体与内核通用的pci结构体的绑定内核负责通用部分设备发现、资源分配、总线管理、生命周期管理驱动负责私有部分设备寄存器操作、功能实现、用户接口这块也是Linux内核哲学的一部分本来一个简单的思路是自定义的驱动程序直接把结构暴露出来(比如基地址offset之类的用户直接读写该位置上的数据即可完成对外设的交互访问)但是真实的情况往往更复杂要考虑到诸如并发生命周期管理热插拔等等这些不是说用户自定义搞不了只是自己单独搞很复杂也容易乱。所以内核设计了一套规范自己写的驱动只是在这套规范上删删改改这样只用专注实现自己相加的功能借助于这个pci子系统可以很方便的完成设备发现热插拔并发等等问题代价是所有的这些个非事务层的处理内存空间申请、probe函数执行、remove函数执行等等都要向内核进程报告最终的执行命令也是由子系统去下发的。并且还需要把自己的变量与子系统的那边的变量进行绑定。也就是由 “用户”《》“自定义驱动”变成插入了第三者第三者pci负责一些宏观上的调控诸如资源分配并发冲突等“用户”《》PCI子系统《》“自定义驱动”/* * probe 路径 * PCI core 发现 1234:11e8 与 edu_pci_ids 匹配后调用这里。 */ static int edu_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct edu_pci_dev *edu; resource_size_t start; int ret; u32 id_value; dev_info(pdev-dev, probe vendor%04x device%04x\n, pdev-vendor, pdev-device); /* 1. 启用 PCI 设备允许访问 BAR 资源。 */ ret pcim_enable_device(pdev); if (ret) { dev_err(pdev-dev, pcim_enable_device failed: %d\n, ret); return ret; } /* 2. 请求并映射 BAR0后续通过 edu-bar0 访问 MMIO 寄存器。 */ ret pcim_iomap_regions(pdev, BIT(EDU_BAR), edu_pci); if (ret) { dev_err(pdev-dev, pcim_iomap_regions BAR%d failed: %d\n, EDU_BAR, ret); return ret; } /* 3. 分配本设备私有结构生命周期绑定到 pdev-dev。 */ edu devm_kzalloc(pdev-dev, sizeof(*edu), GFP_KERNEL); if (!edu) return -ENOMEM; edu-pdev pdev; /* 4. 从 pcim_iomap_table 中取出 BAR0 的映射地址。 */ edu-bar0 pcim_iomap_table(pdev)[EDU_BAR]; if (!edu-bar0) return -ENOMEM; /* 5. 保存 BAR0 长度供 ioctl offset 合法性检查使用。 */ edu-bar0_len pci_resource_len(pdev, EDU_BAR); if (edu-bar0_len EDU_MMIO_SIZE) { dev_err(pdev-dev, BAR%d too small: %pa\n, EDU_BAR, edu-bar0_len); return -ENODEV; } /* 6. 关联 pdev 和 eduremove/debug 等路径可通过 pci_get_drvdata 取回。 */ pci_set_drvdata(pdev, edu); return 0; }1.1.1.4 剩下的就是remove函数用户在shell中输入这个rmmod edu_pci时候这个是移除驱动的指令会先通知到pci子系统这个时候pci子系统就会直接执行删除驱动的能力包括一些之前probe函数申请的内存空间都可以很方便的交给内核去释放然后子系统还会下放执行用户自定义的移除驱动函数在这里定义成了直接打印一个remove就行static void edu_pci_remove(struct pci_dev *pdev) { dev_info(pdev-dev, remove\n); }1.1.2 字符设备驱动骨架1.1.2.1 宏定义/* 当前先暴露两个最基础寄存器ID 寄存器和 liveness 测试寄存器。 */ #define EDU_REG_ID 0x00 #define EDU_REG_LIVENESS 0x04 /* 最多支持 256 个 edu 设备实例对应 /dev/edu0 ~ /dev/edu255。 */ #define EDU_MAX_DEVICES 256Linux中的ioctl是个组合字段 bit 31-30: 数据传输方向dir bit 29-16: 数据大小size bit 15-8: 魔数type/magic bit 7-0: 命令序号nr字段含义作用dir数据传输方向告诉内核是用户态写数据到内核还是内核读数据给用户态size数据大小告诉内核这次 ioctl 传输的数据块大小字节type魔数Magic Number用来区分不同设备的命令集避免冲突nr命令序号同一个设备的不同命令的编号①魔数定义EDU_IOC_MAGIC e#define EDU_IOC_MAGIC e作用这是 EDU 设备 ioctl 命令集的唯一标识。用一个字符ASCII 码作为魔数是 Linux 驱动的标准做法每个设备的魔数应该是唯一的避免和其他设备的 ioctl 命令冲突这里用e是因为 edu 的首字母非常直观为什么需要魔数如果没有魔数你可能会不小心给两个不同的设备定义了相同的命令号导致内核把发给 A 设备的命令错误地交给 B 设备处理引发严重问题。魔数就是用来解决这个冲突的。② 读 ID 命令EDU_IOC_GET_ID#define EDU_IOC_GET_ID _IOR(EDU_IOC_MAGIC, 0x00, __u32)拆解_IOR表示这是一个读命令数据从内核流向用户态EDU_IOC_MAGIC魔数e0x00命令序号 0__u32数据大小是 4 字节32 位无符号整数功能用户态调用这个 ioctl内核会把 EDU 设备的 ID 寄存器值4 字节返回给用户态。③读寄存器命令EDU_IOC_REG_READ#define EDU_IOC_REG_READ _IOWR(EDU_IOC_MAGIC, 0x01, struct edu_ioc_reg)拆解_IOWR表示这是一个双向传输命令先写用户态把要读的寄存器偏移量传给内核后读内核把寄存器的值返回给用户态EDU_IOC_MAGIC魔数e0x01命令序号 1struct edu_ioc_reg数据大小是这个结构体的大小8 字节为什么用 _IOWR因为读寄存器需要两个步骤用户态告诉内核我要读偏移量为 X 的寄存器内核告诉用户态偏移量 X 的寄存器值是 Y这是一个先写后读的双向过程所以必须用_IOWR。④写寄存器命令EDU_IOC_REG_WRITE#define EDU_IOC_REG_WRITE _IOW(EDU_IOC_MAGIC, 0x02, struct edu_ioc_reg)拆解_IOW表示这是一个写命令数据从用户态流向内核EDU_IOC_MAGIC魔数e0x02命令序号 2struct edu_ioc_reg数据大小是 8 字节功能用户态把要写的寄存器偏移量和值传给内核内核把值写入对应的寄存器。/* * 字符设备 ioctl 命令定义。 * * 目前只做最小可用接口 * - EDU_IOC_GET_ID读取 edu 设备 ID 寄存器。 * - EDU_IOC_REG_READ读取任意 32-bit 对齐的 BAR0 寄存器。 * - EDU_IOC_REG_WRITE写入任意 32-bit 对齐的 BAR0 寄存器。 */ #define EDU_IOC_MAGIC e #define EDU_IOC_GET_ID _IOR(EDU_IOC_MAGIC, 0x00, __u32) #define EDU_IOC_REG_READ _IOWR(EDU_IOC_MAGIC, 0x01, struct edu_ioc_reg) #define EDU_IOC_REG_WRITE _IOW(EDU_IOC_MAGIC, 0x02, struct edu_ioc_reg) /* 用户态通过 ioctl 传入/传出的寄存器访问参数。 */ struct edu_ioc_reg { __u32 offset; __u32 value; };1.1.2.2 成员变量的声明instance的作用是在多个edu设备下进行区分当插入多个 EDU 设备时内核会为每个设备调用一次edu_pci_probe函数。如果没有instance所有设备都会尝试创建/dev/edu0导致冲突只有第一个设备能成功注册。使用内核提供的IDAID Allocator机制来分配instancemisc_name[32]是个char类型的数组用来存储设备节点的名字比如有的叫edu0,有的叫edu22,32的尺寸大小是一个冗余设计。struct edu_pci_dev { struct pci_dev *pdev; void __iomem *bar0; resource_size_t bar0_len; int instance; struct miscdevice miscdev; char misc_name[32]; }; /* ida 用来给多个 edu 设备分配稳定的小编号edu0、edu1、... */ static DEFINE_IDA(edu_ida);1.1.2.3对字符设备的open\read\write\ioctl对之前暴露出来的寄存器 进行读写以及ioctl都是差不多的①应该先open的然后再进行其余三者的操作用户态open(/dev/edu0, O_RDWR) ↓ 内核VFS层创建 struct file 对象 ↓ VFS层调用 misc 驱动的 open 函数 ↓ 进入你的 edu_open 函数依旧是用户态发指令指令会先上传到字符设备驱动的子系统中经系统的处理此处子系统会创建一个file类型的通用变量再下发执行open的指令到自定义的驱动进行具体事务的处理在这里我们的具体事务是将自定义的edu_pci_dev类型的变量edu与file中private_data进行绑定处理。绑定完之后VFS虚拟文件系统可以对edu直接进行操作的static int edu_open(struct inode *inode, struct file *file) { // 1. 从 file-private_data 拿到 miscdevice 指针 // 这是 misc 驱动框架提前帮你设置好的 struct miscdevice *miscdev file-private_data; // 2. 用 container_of 从 miscdev 找回我们的 edu_pci_dev struct edu_pci_dev *edu container_of(miscdev, struct edu_pci_dev, miscdev); // 3. ✅ 关键把 edu 指针赋值给 file-private_data file-private_data edu; return 0; }②read write ioctl整体都是差不多逻辑的都是先通过file-private_data获取edu设备的然后对edu设备上指定位置的数据进行读取或者是写入当然也有一些越界写入边界检查等处理逻辑。/* ida 用来给多个 edu 设备分配稳定的小编号edu0、edu1、... */ static DEFINE_IDA(edu_ida); /* MMIO 读写小封装避免到处直接写 edu-bar0 offset。 */ static inline u32 edu_readl(struct edu_pci_dev *edu, u32 offset) { return ioread32(edu-bar0 offset); } static inline void edu_writel(struct edu_pci_dev *edu, u32 offset, u32 value) { iowrite32(value, edu-bar0 offset); } /* ioctl 允许用户指定 offset因此这里统一检查越界和 32-bit 对齐。 */ static bool edu_reg_offset_valid(struct edu_pci_dev *edu, u32 offset) { return offset edu-bar0_len - sizeof(u32) IS_ALIGNED(offset, sizeof(u32)); } /* * open 路径 * misc 框架进入 open 时file-private_data 默认指向 struct miscdevice。 * 这里用 container_of 找回外层 edu_pci_dev后续 read/write/ioctl 直接使用。 */ static int edu_open(struct inode *inode, struct file *file) { struct miscdevice *miscdev file-private_data; struct edu_pci_dev *edu container_of(miscdev, struct edu_pci_dev, miscdev); file-private_data edu; return 0; } /* * read 路径 * 让用户可以直接 cat /dev/edu0看到 ID 和 liveness 寄存器当前值。 */ static ssize_t edu_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { struct edu_pci_dev *edu file-private_data; char text[96]; int len; len scnprintf(text, sizeof(text), id0x%08x liveness0x%08x\n, edu_readl(edu, EDU_REG_ID), edu_readl(edu, EDU_REG_LIVENESS)); return simple_read_from_buffer(buf, count, ppos, text, len); } /* * write 路径 * 用户 echo 一个数字到 /dev/edu0 时把这个值写入 liveness 寄存器。 */ static ssize_t edu_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { struct edu_pci_dev *edu file-private_data; char text[32]; unsigned long value; int ret; if (count sizeof(text)) return -EINVAL; if (copy_from_user(text, buf, count)) return -EFAULT; text[count] \0; ret kstrtoul(text, 0, value); if (ret) return ret; edu_writel(edu, EDU_REG_LIVENESS, (u32)value); return count; } /* * ioctl 路径 * 给 C 测试程序使用比 read/write 更适合表达“读/写某个寄存器”。 */ static long edu_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct edu_pci_dev *edu file-private_data; struct edu_ioc_reg reg; __u32 value; if (_IOC_TYPE(cmd) ! EDU_IOC_MAGIC) return -ENOTTY; switch (cmd) { case EDU_IOC_GET_ID: value edu_readl(edu, EDU_REG_ID); if (copy_to_user((void __user *)arg, value, sizeof(value))) return -EFAULT; return 0; case EDU_IOC_REG_READ: if (copy_from_user(reg, (void __user *)arg, sizeof(reg))) return -EFAULT; if (!edu_reg_offset_valid(edu, reg.offset)) return -EINVAL; reg.value edu_readl(edu, reg.offset); if (copy_to_user((void __user *)arg, reg, sizeof(reg))) return -EFAULT; return 0; case EDU_IOC_REG_WRITE: if (copy_from_user(reg, (void __user *)arg, sizeof(reg))) return -EFAULT; if (!edu_reg_offset_valid(edu, reg.offset)) return -EINVAL; edu_writel(edu, reg.offset, reg.value); return 0; default: return -ENOTTY; } }1.1.2.4 probe函数中对字符设备的注册/* /dev/eduN 支持的文件操作集合。 */ static const struct file_operations edu_fops { .owner THIS_MODULE, .open edu_open, .read edu_read, .write edu_write, .unlocked_ioctl edu_ioctl, .llseek no_llseek, };start pci_resource_start(pdev, EDU_BAR); dev_info(pdev-dev, BAR%d start%pa len%pa mapped%p\n, EDU_BAR, start, edu-bar0_len, edu-bar0); id_value edu_readl(edu, EDU_REG_ID); dev_info(pdev-dev, MMIO[0x00]0x%08x\n, id_value); edu_writel(edu, EDU_REG_LIVENESS, 0xa5a5a5a5); dev_info(pdev-dev, MMIO[0x04]0x%08x\n, edu_readl(edu, EDU_REG_LIVENESS)); /* 8. 分配字符设备实例编号单设备时通常就是 0对应 /dev/edu0。 */ ret ida_alloc_max(edu_ida, EDU_MAX_DEVICES - 1, GFP_KERNEL); if (ret 0) return ret; edu-instance ret; /* 9. 注册 misc 字符设备成功后 devtmpfs 会创建 /dev/eduN。 */ snprintf(edu-misc_name, sizeof(edu-misc_name), edu%d, edu-instance); edu-miscdev.minor MISC_DYNAMIC_MINOR; edu-miscdev.name edu-misc_name; edu-miscdev.fops edu_fops; edu-miscdev.parent pdev-dev; ret misc_register(edu-miscdev); if (ret) { //注册失败时释放实例编号避免下次 probe 时分配到同一个编号导致 /dev/eduN 冲突。 dev_err(pdev-dev, misc_register failed: %d\n, ret); ida_free(edu_ida, edu-instance); return ret; } dev_info(pdev-dev, registered /dev/%s\n, edu-misc_name);1.2 字符设备驱动成功效果字符设备注册读写测试补充QEMU edu 设备的 liveness 寄存器行为写入一个值后读回来会返回按位取反的结果所以 0x0000ffff变成0xffff0000这正好说明 MMIO 写入确实到达了虚拟设备。