SPI驱动学习三(spidev的使用)

2024-09-02 02:12
文章标签 学习 使用 驱动 spi spidev

本文主要是介绍SPI驱动学习三(spidev的使用),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 一、 spidev驱动程序分析
    • 1. 驱动框架
    • 2. 驱动程序分析
  • 二、SPI应用程序分析
    • 1. 使用方法
    • 2. 代码分析
      • 2.1 显示设备属性
      • 2.2 读数据
      • 2.3 先写再读
      • 2.4 同时读写
    • 3. SPI应用编程详解
    • 4. spidev的缺点

一、 spidev驱动程序分析

参考资料:
* 内核驱动:`drivers\spi\spidev.c`
* 内核提供的测试程序:`tools\spi\spidev_fdx.c`
* 内核文档:`Documentation\spi\spidev`

1. 驱动框架

  设备树示例:

spidev0: spidev@0 {compatible = “spidev”;reg = <0>;spi-max-frequency = <50000000>;
};

下图请双击放大查看!!!
在这里插入图片描述

  设备树里某个spi设备节点的compatible属性等于下列值,就会跟spidev驱动匹配:

#ifdef CONFIG_OF
static const struct of_device_id spidev_dt_ids[] = {{ .compatible = "rohm,dh2228fv" },{ .compatible = "lineartechnology,ltc2488" },{ .compatible = "ge,achc" },{ .compatible = "semtech,sx1301" },{ .compatible = "lwn,bk4" },{ .compatible = "dh,dhcom-board" },{ .compatible = "menlo,m53cpld" },{ .compatible = "rockchip,spidev" },{},
};
MODULE_DEVICE_TABLE(of, spidev_dt_ids);
#endif

  匹配之后,spidev.c的spidev_probe会被调用,它会:

  • 分配一个spidev_data结构体,用来记录对应的spi_device
  • spidev_data会被记录在一个链表里
  • 分配一个次设备号,以后可以根据这个次设备号在链表里找到spidev_data
  • device_create:这会生产一个设备节点/dev/spidevB.D,B表示总线号,D表示它是这个SPI Master下第几个设备。然后,我们就可以通过/dev/spidevB.D来访问spidev驱动程序。

2. 驱动程序分析

  spidev.c通过file_operations向APP提供接口,上层通过这些接口操作SPI设备

static const struct file_operations spidev_fops = {.owner = THIS_MODULE,.write = spidev_write, //单工方式进行写.read = spidev_read, //单工方式进行读.unlocked_ioctl = spidev_ioctl, //实现对SPI设备的特定控制操作,设置//频率、模式,进行双工传输(可以同时读写).compat_ioctl = spidev_compat_ioctl, //用于处理兼容模式下的控制命令.open = spidev_open, //实现打开SPI设备时的操作.release = spidev_release, //设备释放回调函数, 实现释放SPI设备时的操作.llseek = no_llseek, //设备不支持的llseek操作,SPI设备不支持文件指针定位操作
};

  为了便于大家深入学习,我借助阿里的通义灵码插件对整个文件代码进行了注释,在这里分享给大家:

// SPDX-License-Identifier: GPL-2.0-or-later
/** Simple synchronous userspace interface to SPI devices*  * Copyright (C) 2006 SWAPP* Andrea Paterniani <a.paterniani@swapp-eng.it>* Copyright (C) 2007 David Brownell (simplification, cleanup)*/
// 必要的头文件,包含Linux内核的各种基本功能和数据结构定义
#include <linux/init.h>
#include <linux/module.h>
#include <linux/ioctl.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/list.h>
#include <linux/errno.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/compat.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/acpi.h>// SPI总线和设备驱动相关头文件
#include <linux/spi/spi.h>
#include <linux/spi/spidev.h>// 用户空间内存访问操作头文件,copy_from_user等函数
#include <linux/uaccess.h>
/** 这个模块支持通过标准用户空间I/O调用来访问SPI设备。* 需要注意的是,尽管传统的UNIX/POSIX I/O语义是半双工的,并且常常掩盖消息边界,* 但完整的SPI支持需要全双工传输。存在几种类型的内部消息边界,用于管理和* 处理其他协议选项。** SPI分配了一个字符设备主号。我们使用位掩码动态分配次号。* 由于没有固定的次号与特定的SPI总线或设备关联,你必须使用热插拔工具,* 如udev(或busybox中的mdev)来创建和销毁/dev/spidevB.C设备节点。*/
#define SPIDEV_MAJOR 153 /* 分配的SPI设备主号 */
#define N_SPI_MINORS 32 /* 每个SPI总线上的设备数量,最多可支持256个 */// 声明一个名为minors的位图,用于跟踪或管理N_SPI_MINORS个次要设备号的使用情况
// 该位图用于在内核空间高效地管理SPI次要设备号的分配与释放
static DECLARE_BITMAP(minors, N_SPI_MINORS);
/* * SPI设备模式管理的位掩码。注意,对于一些设置的不正确配置可能会给共享总线上的其他设备带来大量问题:**  - CS_HIGH ... 当不应激活此设备时,它将处于活动状态*  - 3WIRE ... 当激活时,它的行为将不符合预期*  - NO_CS ... 没有明确的消息边界;这与共享总线模型完全不兼容*  - READY ... 当不应进行传输时,传输可能会继续** 重新考虑:更改这些标志应该是特权操作吗?*/
#define SPI_MODE_MASK                                                          \(SPI_CPHA | SPI_CPOL | SPI_CS_HIGH | SPI_LSB_FIRST | SPI_3WIRE |       \SPI_LOOP | SPI_NO_CS | SPI_READY | SPI_TX_DUAL | SPI_TX_QUAD |        \SPI_TX_OCTAL | SPI_RX_DUAL | SPI_RX_QUAD | SPI_RX_OCTAL)// 结构体spidev_data用于描述SPI设备的属性和状态
struct spidev_data {// devt代表设备号,用于设备在系统中的唯一标识dev_t devt;// spi_lock用于同步对SPI设备的访问,以防止并发访问问题spinlock_t spi_lock;// spi指向实际的SPI设备实例struct spi_device *spi;// device_entry用于将此设备注册到全局设备列表中struct list_head device_entry;// TX/RX缓冲区仅在设备打开(用户数大于0)时才被分配// buf_lock用于同步对tx_buffer和rx_buffer的访问struct mutex buf_lock;// users记录当前打开此设备的用户数unsigned users;// tx_buffer和rx_buffer分别用于SPI通信的发送和接收缓冲区u8 *tx_buffer;u8 *rx_buffer;// speed_hz表示SPI通信的时钟速度u32 speed_hz;
};static LIST_HEAD(device_list);
static DEFINE_MUTEX(device_list_lock);static unsigned bufsiz = 4096;
// 注册模块参数 bufsiz,类型为无符号整数,权限为 S_IRUGO
module_param(bufsiz, uint, S_IRUGO);
// 描述模块参数 bufsiz 的作用:用于指定最大支持的 SPI 消息中的数据字节数
MODULE_PARM_DESC(bufsiz, "data bytes in biggest supported SPI message");
/*-------------------------------------------------------------------------*//*** spidev_sync - 同步SPI设备通信* @spidev: SPI设备数据结构指针* @message: 指向SPI消息的指针** 本函数的目的是将SPI消息同步到SPI设备。它首先通过自旋锁spi_lock来保护* SPI设备指针spi的访问,以确保在多线程环境下的一致性。然后,它检查spi设备* 是否有效。如果有效,则调用spi_sync函数发送和接收数据。如果spi设备无效,* 则返回-ESHUTDOWN错误码。最后,如果spi_sync调用成功(即状态为0),则返回* 消息的实际长度。** 返回值:* - 在成功发送消息时返回消息的实际长度。* - 在spi设备无效时返回-ESHUTDOWN错误码。* - 在其他错误发生时返回相应的错误码。*/
static ssize_t spidev_sync(struct spidev_data *spidev,struct spi_message *message)
{int status;struct spi_device *spi;// 加锁以保护对spidev->spi的访问spin_lock_irq(&spidev->spi_lock);spi = spidev->spi;spin_unlock_irq(&spidev->spi_lock);// 检查spi设备是否为空if (spi == NULL)status = -ESHUTDOWN;else// 调用spi_sync进行同步操作status = spi_sync(spi, message);// 如果spi_sync成功,返回实际长度;否则返回状态码if (status == 0)status = message->actual_length;return status;
}/*** 同步写入SPI设备* * 该函数通过SPI协议同步地将数据从内核空间写入到SPI设备中。* 它设置传输参数,如传输的字节数和速度,然后执行传输。* * @param spidev 指向包含SPI设备信息和传输参数的结构体的指针* @param len 要写入SPI设备的字节数* * @return 返回实际写入的字节数;在发生错误时返回负值*/
static inline ssize_t spidev_sync_write(struct spidev_data *spidev, size_t len)
{// 初始化spi_transfer结构体,设置传输参数struct spi_transfer t = {.tx_buf = spidev->tx_buffer,.len = len,.speed_hz = spidev->speed_hz,};struct spi_message m;// 初始化spi_message结构体spi_message_init(&m);// 将spi_transfer添加到spi_message中spi_message_add_tail(&t, &m);// 执行同步SPI传输return spidev_sync(spidev, &m);
}/*** 使用同步方式读取SPI设备数据* * 该函数构建一个spi_transfer结构体,其中包含待读取数据的缓冲区、数据长度以及速度等信息,* 并将其添加到spi_message结构体中,随后通过调用spidev_sync函数来执行实际的SPI读操作* * @param spidev SPI设备数据结构指针,包含设备状态及配置信息* @param len 计划读取的数据长度* @return 返回实际读取的字节数,如果发生错误则返回负值*/
static inline ssize_t spidev_sync_read(struct spidev_data *spidev, size_t len)
{// 初始化spi_transfer结构体,配置读取操作的参数struct spi_transfer t = {.rx_buf = spidev->rx_buffer,.len = len,.speed_hz = spidev->speed_hz,};struct spi_message m;// 初始化spi_message结构体spi_message_init(&m);// 将spi_transfer结构体添加到spi_message中,构成一次SPI传输操作spi_message_add_tail(&t, &m);// 调用spidev_sync函数执行SPI同步读操作return spidev_sync(spidev, &m);
}/*-------------------------------------------------------------------------*//* Read-only message with current device setup */
/*** 从 SPI 设备读取数据* * 此函数实现了从 SPI (Serial Peripheral Interface) 设备读取数据的功能它被设计为在 Linux 内核环境中使用,* 通过文件操作接口暴露给用户空间程序* * @param filp 指向文件的结构体,包含私有数据等信息* @param buf 用户空间提供的缓冲区,用于存放读取的数据* @param count 用户空间请求读取的数据长度* @param f_pos 文件位置指针,通常不使用* * @return 返回实际读取的字节数,或者在发生错误时返回负值错误代码* * 主要功能包括:* - 检查并限制单次读取的数据长度,防止缓冲区溢出* - 使用互斥锁保护共享的缓冲区,确保数据读取的同步安全性* - 调用底层函数进行数据读取,并将读取的数据拷贝到用户空间*/
static ssize_t spidev_read(struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
{// 检查并限制单次读取的数据长度,防止缓冲区溢出if (count > bufsiz)return -EMSGSIZE;// 获取 SPI 设备数据结构的指针struct spidev_data *spidev = filp->private_data;// 使用互斥锁保护共享的缓冲区,确保数据读取的同步安全性mutex_lock(&spidev->buf_lock);// 调用底层函数进行数据读取ssize_t status = spidev_sync_read(spidev, count);// 如果成功读取到数据if (status > 0) {// 将读取的数据拷贝到用户空间unsigned long missing =copy_to_user(buf, spidev->rx_buffer, status);// 如果拷贝失败,则返回错误代码if (missing == status)status = -EFAULT;// 否则返回实际成功拷贝的数据长度elsestatus = status - missing;}// 释放互斥锁mutex_unlock(&spidev->buf_lock);// 返回实际读取的字节数,或者在发生错误时返回负值错误代码return status;
}/* Write-only message with current device setup */
/*** spidev_write - 向SPI设备写入数据* @filp: 文件结构体指针,包含设备私有数据* @buf: 用户空间缓冲区指针,包含待写入的数据* @count: 待写入的数据大小* @f_pos: 文件位置指针,写操作后可能更新* * 本函数处理向SPI设备写入数据的操作。它首先检查请求写入的数据量是否超过缓冲区大小,* 如果超过,则返回-EMSGSIZE错误。然后,它从用户空间缓冲区复制数据到内核空间的缓冲区,* 如果复制成功,调用spidev_sync_write进行实际的SPI写操作。最后,释放缓冲区锁并返回操作状态。* * 锁定buf_lock互斥锁是为了防止同时访问传输缓冲区,确保数据一致性和设备的正确操作。* * 返回值:* - 如果成功,返回写入的字节数。* - 如果发生错误,返回一个负的错误代码,如-EMSGSIZE(消息太大)或-EFAULT(无效地址)。*/
static ssize_t spidev_write(struct file *filp, const char __user *buf,size_t count, loff_t *f_pos)
{struct spidev_data *spidev;ssize_t status;unsigned long missing;// 检查请求写入的数据量是否超过设备缓冲区大小if (count > bufsiz)return -EMSGSIZE;// 获取SPI设备数据指针spidev = filp->private_data;// 加锁以保护缓冲区mutex_lock(&spidev->buf_lock);// 从用户空间复制数据到内核空间缓冲区missing = copy_from_user(spidev->tx_buffer, buf, count);// 根据复制结果,进行相应的处理if (missing == 0)// 复制成功,进行SPI同步写操作status = spidev_sync_write(spidev, count);else// 复制失败,返回错误status = -EFAULT;// 解锁以释放缓冲区保护mutex_unlock(&spidev->buf_lock);return status;
}/** spidev_message - 执行 SPI 转发 (transfer) 操作** 参数:* @spidev: SPI 设备数据结构指针* @u_xfers: 用户空间转发结构体数组指针* @n_xfers: 转发结构体数组的元素数量** 返回: * 成功时返回总的数据长度,错误时返回负的错误码** 本函数负责将用户空间提供的 SPI 转发 (transfer) 请求转换为内核空间的 SPI 消息处理,* 包括分配内核空间转发结构体数组,初始化 SPI 消息,处理 TX 和 RX 数据缓冲区,* 以及实际执行 SPI 转发操作。函数需确保数据的正确传输以及在错误发生时释放相应资源。*/
static int spidev_message(struct spidev_data *spidev,struct spi_ioc_transfer *u_xfers, unsigned n_xfers)
{struct spi_message msg;struct spi_transfer *k_xfers;struct spi_transfer *k_tmp;struct spi_ioc_transfer *u_tmp;unsigned n, total, tx_total, rx_total;u8 *tx_buf, *rx_buf;int status = -EFAULT; // 初始化函数返回状态为错误spi_message_init(&msg); // 初始化 SPI 消息结构k_xfers = kcalloc(n_xfers, sizeof(*k_tmp),GFP_KERNEL); // 为 SPI 转发分配内核空间数组if (k_xfers == NULL)return -ENOMEM; // 分配失败时返回内存不足错误/* 构建 SPI 消息,将任何 TX 数据复制到缓冲区。* 遍历用户空间提供的转发数组,使用每个元素初始化一个内核空间的转发结构。*/tx_buf = spidev->tx_buffer; // 设置 TX 数据缓冲区指针rx_buf = spidev->rx_buffer; // 设置 RX 数据缓冲区指针total = 0; // 总数据长度tx_total = 0; // 已用TX缓冲区长度rx_total = 0; // 已用RX缓冲区长度for (n = n_xfers, k_tmp = k_xfers, u_tmp = u_xfers; n;n--, k_tmp++, u_tmp++) {/* 确保从 rx_buf/tx_buf 分配的空间满足 DMA 对齐要求 */unsigned int len_aligned =ALIGN(u_tmp->len, ARCH_KMALLOC_MINALIGN);k_tmp->len = u_tmp->len; // 设置转发长度total += k_tmp->len; // 更新总长度/* 检查总长度和当前转发长度是否在正整数范围内,避免溢出 */if (total > INT_MAX || k_tmp->len > INT_MAX) {status = -EMSGSIZE;goto done;}if (u_tmp->rx_buf) {/* 当前转发需要 RX 缓冲区空间 */rx_total += len_aligned;if (rx_total > bufsiz) {status = -EMSGSIZE;goto done;}k_tmp->rx_buf = rx_buf; // 设置 RX 缓冲区指针rx_buf += len_aligned; // 更新 RX 缓冲区指针}if (u_tmp->tx_buf) {/* 当前转发需要 TX 缓冲区空间 */tx_total += len_aligned;if (tx_total > bufsiz) {status = -EMSGSIZE;goto done;}k_tmp->tx_buf = tx_buf; // 设置 TX 缓冲区指针/* 从用户空间复制 TX 数据到 TX 缓冲区 */if (copy_from_user(tx_buf,(const u8 __user *)(uintptr_t)u_tmp->tx_buf,u_tmp->len))goto done; // 复制失败时释放资源tx_buf += len_aligned; // 更新 TX 缓冲区指针}/* 将用户空间转发结构体的相关字段复制到内核空间转发结构体 */k_tmp->cs_change = !!u_tmp->cs_change;k_tmp->tx_nbits = u_tmp->tx_nbits;k_tmp->rx_nbits = u_tmp->rx_nbits;k_tmp->bits_per_word = u_tmp->bits_per_word;k_tmp->delay.value = u_tmp->delay_usecs;k_tmp->delay.unit = SPI_DELAY_UNIT_USECS;k_tmp->speed_hz = u_tmp->speed_hz;k_tmp->word_delay.value = u_tmp->word_delay_usecs;k_tmp->word_delay.unit = SPI_DELAY_UNIT_USECS;if (!k_tmp->speed_hz)k_tmp->speed_hz = spidev->speed_hz; // 使用设备默认速度
#ifdef VERBOSE/* 详细调试信息 */dev_dbg(&spidev->spi->dev,"  xfer len %u %s%s%s%dbits %u usec %u usec %uHz\n",k_tmp->len, k_tmp->rx_buf ? "rx " : "",k_tmp->tx_buf ? "tx " : "",k_tmp->cs_change ? "cs " : "",k_tmp->bits_per_word ?: spidev->spi->bits_per_word,k_tmp->delay.value, k_tmp->word_delay.value,k_tmp->speed_hz ?: spidev->spi->max_speed_hz);
#endifspi_message_add_tail(k_tmp, &msg); // 将转发添加到 SPI 消息末尾}/* 调用 SPI 总线控制器执行 SPI 转发操作 */status = spidev_sync(spidev, &msg);if (status < 0)goto done; // 执行失败时释放资源/* 将 RX 数据从缓冲区复制回用户空间 */for (n = n_xfers, k_tmp = k_xfers, u_tmp = u_xfers; n;n--, k_tmp++, u_tmp++) {if (u_tmp->rx_buf) {/* 从内核空间复制 RX 数据到用户空间 */if (copy_to_user((u8 __user *)(uintptr_t)u_tmp->rx_buf,k_tmp->rx_buf, u_tmp->len)) {status = -EFAULT;goto done;}}}status = total; // 返回总的数据长度done:kfree(k_xfers); // 释放内核空间转发数组return status; // 返回执行结果或错误码
}/*** 获取spi_ioc_transfer结构的内核空间副本* * 该函数用于从用户空间获取spi_ioc_transfer结构的消息,并检查相关的ioctl命令参数* 它确保命令的类型、编号和方向都符合预期,并且消息数量有效* * @param cmd ioctl命令,用于与SPI设备通信* @param u_ioc 用户空间中的spi_ioc_transfer结构数组指针,包含要传输的消息* @param n_ioc 指向unsigned变量的指针,用于存储消息的数量* * @return 结构体spi_ioc_transfer的内核空间指针,用于后续的SPI传输操作*         如果发生错误(如命令格式无效或没有消息),则返回ERR_PTR包含错误代码*/
static struct spi_ioc_transfer *
spidev_get_ioc_message(unsigned int cmd, struct spi_ioc_transfer __user *u_ioc,unsigned *n_ioc)
{u32 tmp;// 检查ioctl命令的类型、编号和方向是否与SPI_IOC_MESSAGE命令匹配if (_IOC_TYPE(cmd) != SPI_IOC_MAGIC ||_IOC_NR(cmd) != _IOC_NR(SPI_IOC_MESSAGE(0)) ||_IOC_DIR(cmd) != _IOC_WRITE)return ERR_PTR(-ENOTTY);// 计算spi_ioc_transfer结构体的个数tmp = _IOC_SIZE(cmd);if ((tmp % sizeof(struct spi_ioc_transfer)) != 0)return ERR_PTR(-EINVAL);*n_ioc = tmp / sizeof(struct spi_ioc_transfer);if (*n_ioc == 0)return NULL;// 将用户空间的数据复制到内核空间的临时区域,并返回该区域的指针return memdup_user(u_ioc, tmp);
}/** spidev_ioctl - 处理SPI设备的ioctl操作* * @filp:指向file结构的指针,表示相关的文件* @cmd: ioctl命令代码* @arg: ioctl命令相关的参数* * 该函数处理SPI设备的输入/输出控制请求,包括读取和设置SPI设备的各种参数,* 以及执行SPI传输。它根据命令代码执行不同的操作,如设置SPI模式、* 设置每字节数量、设置速度等。对于写操作,它在更改设备参数之前会检查参数的有效性。* 对于读操作,它从设备中获取请求的信息。该函数还需要处理设备在操作过程中的移除情况。* * 返回: 0表示成功,负值表示错误。*/
static long spidev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{int retval = 0;struct spidev_data *spidev;struct spi_device *spi;u32 tmp;unsigned n_ioc;struct spi_ioc_transfer *ioc;// 检查命令的类型和号码if (_IOC_TYPE(cmd) != SPI_IOC_MAGIC)return -ENOTTY;// 防止在执行ioctl期间设备被移除spidev = filp->private_data;spin_lock_irq(&spidev->spi_lock);spi = spi_dev_get(spidev->spi);spin_unlock_irq(&spidev->spi_lock);if (spi == NULL)return -ESHUTDOWN;// 保护I/O操作和数据字段的一致性mutex_lock(&spidev->buf_lock);switch (cmd) {// 读取请求处理case SPI_IOC_RD_MODE:case SPI_IOC_RD_MODE32:tmp = spi->mode;// 考虑控制器的GPIO描述符使用情况{struct spi_controller *ctlr = spi->controller;if (ctlr->use_gpio_descriptors && ctlr->cs_gpiods &&ctlr->cs_gpiods[spi->chip_select])tmp &= ~SPI_CS_HIGH;}if (cmd == SPI_IOC_RD_MODE)retval = put_user(tmp & SPI_MODE_MASK,(__u8 __user *)arg);elseretval = put_user(tmp & SPI_MODE_MASK,(__u32 __user *)arg);break;case SPI_IOC_RD_LSB_FIRST:retval = put_user((spi->mode & SPI_LSB_FIRST) ? 1 : 0,(__u8 __user *)arg);break;case SPI_IOC_RD_BITS_PER_WORD:retval = put_user(spi->bits_per_word, (__u8 __user *)arg);break;case SPI_IOC_RD_MAX_SPEED_HZ:retval = put_user(spidev->speed_hz, (__u32 __user *)arg);break;// 写入请求处理case SPI_IOC_WR_MODE:case SPI_IOC_WR_MODE32:if (cmd == SPI_IOC_WR_MODE)retval = get_user(tmp, (u8 __user *)arg);elseretval = get_user(tmp, (u32 __user *)arg);if (retval == 0) {struct spi_controller *ctlr = spi->controller;u32 save = spi->mode;if (tmp & ~SPI_MODE_MASK) {retval = -EINVAL;break;}if (ctlr->use_gpio_descriptors && ctlr->cs_gpiods &&ctlr->cs_gpiods[spi->chip_select])tmp |= SPI_CS_HIGH;tmp |= spi->mode & ~SPI_MODE_MASK;spi->mode = (u16)tmp;retval = spi_setup(spi);if (retval < 0)spi->mode = save;elsedev_dbg(&spi->dev, "spi mode %x\n", tmp);}break;case SPI_IOC_WR_LSB_FIRST:retval = get_user(tmp, (__u8 __user *)arg);if (retval == 0) {u32 save = spi->mode;if (tmp)spi->mode |= SPI_LSB_FIRST;elsespi->mode &= ~SPI_LSB_FIRST;retval = spi_setup(spi);if (retval < 0)spi->mode = save;elsedev_dbg(&spi->dev, "%csb first\n",tmp ? 'l' : 'm');}break;case SPI_IOC_WR_BITS_PER_WORD:retval = get_user(tmp, (__u8 __user *)arg);if (retval == 0) {u8 save = spi->bits_per_word;spi->bits_per_word = tmp;retval = spi_setup(spi);if (retval < 0)spi->bits_per_word = save;elsedev_dbg(&spi->dev, "%d bits per word\n", tmp);}break;case SPI_IOC_WR_MAX_SPEED_HZ:retval = get_user(tmp, (__u32 __user *)arg);if (retval == 0) {u32 save = spi->max_speed_hz;spi->max_speed_hz = tmp;retval = spi_setup(spi);if (retval == 0) {spidev->speed_hz = tmp;dev_dbg(&spi->dev, "%d Hz (max)\n",spidev->speed_hz);}spi->max_speed_hz = save;}break;// 默认情况,处理分割的和/或全双工I/O请求default:ioc = spidev_get_ioc_message(cmd, (struct spi_ioc_transfer __user *)arg, &n_ioc);if (IS_ERR(ioc)) {retval = PTR_ERR(ioc);break;}if (!ioc)break; /* n_ioc is also 0 */retval = spidev_message(spidev, ioc, n_ioc);kfree(ioc);break;}mutex_unlock(&spidev->buf_lock);spi_dev_put(spi);return retval;
}#ifdef CONFIG_COMPAT
/** spidev_compat_ioc_message - 处理SPI设备的兼容性 IOCTL消息* * 该函数被设计用来处理SPI设备的IOCTL请求,确保32位用户空间应用程序* 可以在64位内核上与SPI设备交互。主要工作是将用户空间提供的传输信息* (spi_ioc_transfer)复制到内核空间,并处理指针的兼容性问题,然后调用* 相应的函数执行SPI传输。* * @filp:指向file结构的指针,表示被打开的设备* @cmd:未使用,兼容性参数* @arg:用户空间传入的spi_ioc_transfer结构的指针* * 返回值:*   0表示成功,负值表示错误代码*/
static long spidev_compat_ioc_message(struct file *filp, unsigned int cmd,unsigned long arg)
{/* 用户空间的spi_ioc_transfer结构指针 */struct spi_ioc_transfer __user *u_ioc;int retval = 0;struct spidev_data *spidev;struct spi_device *spi;unsigned n_ioc, n;struct spi_ioc_transfer *ioc;/* 将用户空间的指针转换为内核空间的指针 */u_ioc = (struct spi_ioc_transfer __user *)compat_ptr(arg);/* 防止在发送IOCTL期间设备被移除 */spidev = filp->private_data;spin_lock_irq(&spidev->spi_lock);spi = spi_dev_get(spidev->spi);spin_unlock_irq(&spidev->spi_lock);/* 如果设备已移除,返回错误 */if (spi == NULL)return -ESHUTDOWN;/* 锁定缓冲区以防止在传输过程中被释放 */mutex_lock(&spidev->buf_lock);/* 检查消息并复制到临时区域 */ioc = spidev_get_ioc_message(cmd, u_ioc, &n_ioc);if (IS_ERR(ioc)) {retval = PTR_ERR(ioc);goto done;}if (!ioc)goto done; /* 如果没有消息,直接退出 *//* 将用户空间的缓冲区指针转换为内核空间的指针 */for (n = 0; n < n_ioc; n++) {ioc[n].rx_buf = (uintptr_t)compat_ptr(ioc[n].rx_buf);ioc[n].tx_buf = (uintptr_t)compat_ptr(ioc[n].tx_buf);}/* 执行SPI传输 */retval = spidev_message(spidev, ioc, n_ioc);kfree(ioc);done:/* 释放缓冲区锁并释放设备引用 */mutex_unlock(&spidev->buf_lock);spi_dev_put(spi);return retval;
}/*** 兼容性SPI设备控制操作* * 该函数提供老版本用户空间驱动与新SPI通信协议兼容的接口。* 它检查请求的命令是否与SPI的特定IO控制命令匹配,并且该命令是写操作。* 如果命令匹配这些条件,函数将调用专门的消息处理函数。* 否则,它将回退到默认的SPI设备IO控制处理。* * @param filp 文件结构指针,表示SPI设备文件。* @param cmd 用户空间请求的IO控制命令。* @param arg 用户空间提供的参数地址。* * @return 返回0表示成功,负值表示错误。*/
static long spidev_compat_ioctl(struct file *filp, unsigned int cmd,unsigned long arg)
{// 检查命令类型、命令编号和方向是否匹配SPI消息写操作if (_IOC_TYPE(cmd) == SPI_IOC_MAGIC &&_IOC_NR(cmd) == _IOC_NR(SPI_IOC_MESSAGE(0)) &&_IOC_DIR(cmd) == _IOC_WRITE)return spidev_compat_ioc_message(filp, cmd, arg);// 不匹配时,回退到默认的SPI设备IO控制处理return spidev_ioctl(filp, cmd, (unsigned long)compat_ptr(arg));
}
#else
#define spidev_compat_ioctl NULL
#endif /* CONFIG_COMPAT *//*** spidev_open - 打开SPI设备的函数* @inode: 文件系统inode结构的指针* @filp: 文件结构的指针,表示打开的文件** 此函数负责在打开SPI设备时进行必要的初始化工作,包括:* 1. 遍历设备列表以查找与设备号匹配的spidev_data结构。* 2. 为传输和接收缓冲区分配内存,如果尚未分配。* 3. 增加用户计数,并将spidev_data指针保存到文件结构中以供后续使用。** 锁定device_list_lock互斥锁以确保设备列表的遍历是安全的。** 返回0表示成功,或负值表示错误(如-ENXIO表示未找到设备,-ENOMEM表示内存分配失败)。*/
static int spidev_open(struct inode *inode, struct file *filp)
{struct spidev_data *spidev;int status = -ENXIO;// 加锁以保护设备列表mutex_lock(&device_list_lock);// 遍历设备列表,寻找匹配的spidev_data结构list_for_each_entry (spidev, &device_list, device_entry) {if (spidev->devt == inode->i_rdev) {status = 0;break;}}if (status) {// 如果未找到匹配的设备,打印调试信息并跳转到错误处理代码pr_debug("spidev: nothing for minor %d\n", iminor(inode));goto err_find_dev;}// 如果传输缓冲区未分配,尝试为其分配内存if (!spidev->tx_buffer) {spidev->tx_buffer = kmalloc(bufsiz, GFP_KERNEL);if (!spidev->tx_buffer) {// 分配失败,设置错误状态并跳转到错误处理代码status = -ENOMEM;goto err_find_dev;}}// 如果接收缓冲区未分配,尝试为其分配内存if (!spidev->rx_buffer) {spidev->rx_buffer = kmalloc(bufsiz, GFP_KERNEL);if (!spidev->rx_buffer) {// 分配失败,设置错误状态并跳转到错误处理代码status = -ENOMEM;goto err_alloc_rx_buf;}}// 增加用户计数,表示设备正在被使用spidev->users++;// 将spidev_data指针保存到文件结构中filp->private_data = spidev;// 调用相关设备的打开函数stream_open(inode, filp);// 解锁以释放设备列表保护mutex_unlock(&device_list_lock);return 0;err_alloc_rx_buf:// 错误处理:释放已分配的传输缓冲区内存,并将指针置空kfree(spidev->tx_buffer);spidev->tx_buffer = NULL;
err_find_dev:// 解锁以释放设备列表保护mutex_unlock(&device_list_lock);return status;
}/*** 当最后一个使用SPI设备的文件被关闭时,释放SPI设备资源* * @param inode 文件系统中的inode结构指针,标识设备文件* @param filp 文件结构指针,描述打开的文件,用于获取私有数据并清理资源* * @return 返回0表示操作成功*/
static int spidev_release(struct inode *inode, struct file *filp)
{// 用于标识是否需要释放整个设备结构的变量int dofree;// 加锁保护设备列表mutex_lock(&device_list_lock);// 从文件私有数据中获取SPI设备结构,并清空私有数据struct spidev_data *spidev = filp->private_data;filp->private_data = NULL;// 加锁保护SPI设备访问spin_lock_irq(&spidev->spi_lock);// 判断是否已经与底层硬件设备解绑,以决定是否需要释放设备结构dofree = (spidev->spi == NULL);spin_unlock_irq(&spidev->spi_lock);// 用户计数减一,用于跟踪设备使用情况spidev->users--;if (!spidev->users) {// 释放传输和接收缓冲区kfree(spidev->tx_buffer);spidev->tx_buffer = NULL;kfree(spidev->rx_buffer);spidev->rx_buffer = NULL;// 根据情况释放设备结构或重置速度if (dofree)kfree(spidev);elsespidev->speed_hz = spidev->spi->max_speed_hz;}
#ifdef CONFIG_SPI_SLAVE// 在配置了SPI从机模式时,尝试中止SPI从机操作if (!dofree)spi_slave_abort(spidev->spi);
#endif// 解锁设备列表mutex_unlock(&device_list_lock);// 返回成功return 0;
}/** 定义SPI设备的文件操作结构体*/
static const struct file_operations spidev_fops = {.owner = THIS_MODULE,/* * 写操作回调函数* 实现向SPI设备写数据的操作*/.write = spidev_write,/* * 读操作回调函数* 实现从SPI设备读数据的操作*/.read = spidev_read,/* * 设备控制命令回调函数* 实现对SPI设备的特定控制操作*/.unlocked_ioctl = spidev_ioctl,/* * 兼容模式下的设备控制命令回调函数* 用于处理兼容模式下的控制命令*/.compat_ioctl = spidev_compat_ioctl,/* * 设备打开回调函数* 实现打开SPI设备时的操作*/.open = spidev_open,/* * 设备释放回调函数* 实现释放SPI设备时的操作*/.release = spidev_release,/* * 设备不支持的llseek操作* SPI设备不支持文件指针定位操作*/.llseek = no_llseek,
};
/*-------------------------------------------------------------------------*//* The main reason to have this class is to make mdev/udev create the* /dev/spidevB.C character device nodes exposing our userspace API.* It also simplifies memory management.*/static struct class *spidev_class;#ifdef CONFIG_OF
static const struct of_device_id spidev_dt_ids[] = {{ .compatible = "rohm,dh2228fv" },{ .compatible = "lineartechnology,ltc2488" },{ .compatible = "ge,achc" },{ .compatible = "semtech,sx1301" },{ .compatible = "lwn,bk4" },{ .compatible = "dh,dhcom-board" },{ .compatible = "menlo,m53cpld" },{ .compatible = "rockchip,spidev" },{},
};
MODULE_DEVICE_TABLE(of, spidev_dt_ids);
#endif#ifdef CONFIG_ACPI/* Dummy SPI devices not to be used in production systems */
#define SPIDEV_ACPI_DUMMY 1static const struct acpi_device_id spidev_acpi_ids[] = {/** The ACPI SPT000* devices are only meant for development and* testing. Systems used in production should have a proper ACPI* description of the connected peripheral and they should also use* a proper driver instead of poking directly to the SPI bus.*/{ "SPT0001", SPIDEV_ACPI_DUMMY },{ "SPT0002", SPIDEV_ACPI_DUMMY },{ "SPT0003", SPIDEV_ACPI_DUMMY },{},
};
MODULE_DEVICE_TABLE(acpi, spidev_acpi_ids);/*** 通过ACPI表检测SPI设备并进行相应的处理。* * 本函数旨在在系统启动过程中,通过ACPI(Advanced Configuration and Power Interface)* 表来识别并处理SPI设备。ACPI表提供了硬件配置和电源管理信息,有助于操作系统* 在无需深入了解硬件细节的情况下配置系统。* * @param spi 指向spi_device结构体的指针,代表正在被探测的SPI设备。* * 注意:此函数是根据ACPI信息来探测和处理SPI设备的,对于不通过ACPI进行配置的系统或设备,* 它可能不适用或不需要。*/
static void spidev_probe_acpi(struct spi_device *spi)
{// 指向ACPI设备ID结构体的指针,用于匹配设备ID和获取设备数据。const struct acpi_device_id *id;// 检查是否有ACPI伴侣设备,如果没有,则直接返回。// 这里的ACPI伴侣设备指的是在ACPI表中描述的、与当前设备相关的配置信息。if (!has_acpi_companion(&spi->dev))return;// 尝试从ACPI表中匹配当前SPI设备的ID。id = acpi_match_device(spidev_acpi_ids, &spi->dev);if (WARN_ON(!id))return;// 检查是否匹配到了虚拟设备(非生产环境设备),并发出警告。// 这种检查是为了确保不会在生产系统中误用不适合的驱动程序。if (id->driver_data == SPIDEV_ACPI_DUMMY)dev_warn(&spi->dev,"do not use this driver in production systems!\n");
}
#else
static inline void spidev_probe_acpi(struct spi_device *spi)
{
}
#endif/*-------------------------------------------------------------------------*//*** spidev_probe - SPI设备探测函数* @spi: 指向spi_device结构体的指针,描述SPI设备** 该函数负责探测和初始化SPI设备,包括分配设备私有数据、初始化锁、* 创建设备节点以及设置驱动私有数据。该函数还会检查设备树中SPI设备的兼容性字符串。* * 返回:* -ENOMEM: 如果分配spidev失败* -ENODEV: 如果没有可用的次要号码* 0: 探测成功*/
static int spidev_probe(struct spi_device *spi)
{struct spidev_data *spidev;int status;unsigned long minor;/** 警告:如果设备树(DT)中列出了spidev且没有特定的兼容性字符串,这可能是DT的错误。* spidev是一个Linux实现的细节,而不是硬件描述的一部分。*/WARN(spi->dev.of_node &&of_device_is_compatible(spi->dev.of_node, "spidev"),"%pOF: buggy DT: spidev listed directly in DT\n",spi->dev.of_node);// 探测ACPI表以获取SPI设备信息spidev_probe_acpi(spi);// 分配驱动程序数据spidev = kzalloc(sizeof(*spidev), GFP_KERNEL);if (!spidev)return -ENOMEM;// 初始化驱动程序数据spidev->spi = spi;spin_lock_init(&spidev->spi_lock);mutex_init(&spidev->buf_lock);// 初始化设备列表头INIT_LIST_HEAD(&spidev->device_entry);// 尝试分配一个次要号码给这个设备mutex_lock(&device_list_lock);minor = find_first_zero_bit(minors, N_SPI_MINORS);if (minor < N_SPI_MINORS) {// 创建设备节点spidev->devt = MKDEV(SPIDEV_MAJOR, minor);// 创建代表SPI设备的device入口// 参数spidev_class表示设备类,&spi->dev表示父设备,spidev->devt为设备编号,// spidev为设备私有数据的指针,"spidev%d.%d"为设备名称格式,spi->master->bus_num和// spi->chip_select用于格式化设备名称dev = device_create(spidev_class, &spi->dev, spidev->devt, spidev,"spidev%d.%d", spi->master->bus_num,spi->chip_select); //会被创建在/sys/class/spidev目录下,并以spidev{master_bus_num}.{chip_select}的形式命名。// 获取device_create函数的返回状态,用于后续判断或处理// PTR_ERR_OR_ZERO宏将返回指针的错误代码或0,这里用于简化错误处理逻辑status = PTR_ERR_OR_ZERO(dev);} else {// 没有可用的次要号码dev_dbg(&spi->dev, "no minor number available!\n");status = -ENODEV;}if (status == 0) {// 标记次要号码为已使用set_bit(minor, minors);// 将设备添加到列表中list_add(&spidev->device_entry, &device_list);}mutex_unlock(&device_list_lock);// 设置SPI速度spidev->speed_hz = spi->max_speed_hz;// 根据探测结果设置驱动私有数据或释放分配的内存if (status == 0)spi_set_drvdata(spi, spidev);elsekfree(spidev);return status;
}/*** 从系统中移除一个SPI设备* * 此函数用于处理SPI设备的移除操作。它首先防止新的打开操作,* 然后确保当前打开的文件描述符上的操作可以干净地中断。接着,* 它从设备列表中删除该设备,并销毁相应的设备实例。如果该设备* 没有被任何进程打开,则释放设备相关的内存。* * @param spi 指向要移除的SPI设备的指针* * @return 返回0表示操作成功*/
static int spidev_remove(struct spi_device *spi)
{/* 获取SPI设备的私有数据 */struct spidev_data *spidev = spi_get_drvdata(spi);/* 上锁防止新的打开操作 */mutex_lock(&device_list_lock);/* 上锁并使能中断,确保当前打开的文件描述符操作可正常中断 */spin_lock_irq(&spidev->spi_lock);/* 断开与spidev的spi_master连接 */spidev->spi = NULL;/* 解锁并使能中断 */spin_unlock_irq(&spidev->spi_lock);/* 从设备列表中删除该设备 */list_del(&spidev->device_entry);/* 销毁设备实例 */device_destroy(spidev_class, spidev->devt);/* 清除设备在minors数组中的位标识 */clear_bit(MINOR(spidev->devt), minors);/* 如果没有用户打开该设备,则释放设备内存 */if (spidev->users == 0)kfree(spidev);/* 解锁 */mutex_unlock(&device_list_lock);return 0;
}// 定义一个静态结构体变量spidev_spi_driver,用于SPI驱动的配置
static struct spi_driver spidev_spi_driver = {// 配置驱动的基本属性.driver = {// 驱动的名称.name =		"spidev",// 设备树匹配表,用于自动探测设备.of_match_table = of_match_ptr(spidev_dt_ids),// ACPI匹配表,用于ACPI兼容设备的自动探测.acpi_match_table = ACPI_PTR(spidev_acpi_ids),},// 指定探针和移除函数.probe =	spidev_probe, // 设备探针函数,当设备被系统识别时调用.remove =	spidev_remove, // 设备移除函数,当设备从系统中移除时调用// 注意:不需要实现挂起/恢复方法// 该驱动除了将请求传递给底层控制器外,不做其他事情// 大部分问题由系统处理,控制器驱动处理剩余问题
};/*-------------------------------------------------------------------------*//** 函数名: spidev_init* 功能: SPI设备驱动初始化函数* 参数: 无* 返回值: int 类型,表示初始化的成功(0)或失败(非0)* 描述: 该函数在模块加载时调用,负责注册字符设备、创建设备类和注册SPI驱动。*       它首先注册字符设备号,然后创建设备类以便udev/mdev能基于这些设备号添加或移除/dev节点,*       最后注册SPI驱动来管理这些设备号。*/
static int __init spidev_init(void)
{int status;/* 确保SPI设备的次设备号数量不超过256 */BUILD_BUG_ON(N_SPI_MINORS > 256);/* 注册字符设备号,"spi"为设备名,spidev_fops为文件操作集合 */status = register_chrdev(SPIDEV_MAJOR, "spi", &spidev_fops);if (status < 0)return status;/* 创建设备类,用于udev/mdev识别和动态添加/移除/dev节点 *//* 设备类通常用于在 /sys/class 目录中创建一个设备类别,以便用户空间工具可以访问和管理这些设备。 */spidev_class = class_create(THIS_MODULE, "spidev");if (IS_ERR(spidev_class)) {unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);return PTR_ERR(spidev_class);}/* 注册SPI驱动 */status = spi_register_driver(&spidev_spi_driver);if (status < 0) {/* 如果注册失败,清理已创建的资源 */class_destroy(spidev_class);unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);}return status;
}
module_init(spidev_init);static void __exit spidev_exit(void)
{spi_unregister_driver(&spidev_spi_driver);class_destroy(spidev_class);unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
}
module_exit(spidev_exit);MODULE_AUTHOR("Andrea Paterniani, <a.paterniani@swapp-eng.it>");
MODULE_DESCRIPTION("User mode SPI device interface");
MODULE_LICENSE("GPL");
MODULE_ALIAS("spi:spidev");

二、SPI应用程序分析

  内核提供的测试程序:tools\spi\spidev_fdx.c,这里不提供源码,大家自己打开内核源码这个文件,学习驱动源码应该都有。

1. 使用方法

spidev_fdx [-h] [-m N] [-r N] /dev/spidevB.D
  • -h: 打印用法
  • -m N:先写1个字节0xaa,再读N个字节,注意:不是同时写同时读
  • -r N:读N个字节

2. 代码分析

2.1 显示设备属性

在这里插入图片描述

2.2 读数据

在这里插入图片描述

2.3 先写再读

在这里插入图片描述

2.4 同时读写

在这里插入图片描述

3. SPI应用编程详解

  请阅读我写的另一篇博文:Linux: SPI应用编程

4. spidev的缺点

  • 使用read、write函数时,只能读、写,这是半双工方式
  • 使用ioctl可以达到全双工的读写
  • 但是spidev有2个缺点:
    • 不支持中断
    • 只支持同步操作,不支持异步操作:就是read/write/ioctl这些函数只能执行完毕才可返回

  本文章参考了韦东山老师驱动大全部分笔记,其余内容为自己整理总结而来。水平有限,欢迎各位在评论区指导交流!!!😁😁😁

这篇关于SPI驱动学习三(spidev的使用)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1128703

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

pdfmake生成pdf的使用

实际项目中有时会有根据填写的表单数据或者其他格式的数据,将数据自动填充到pdf文件中根据固定模板生成pdf文件的需求 文章目录 利用pdfmake生成pdf文件1.下载安装pdfmake第三方包2.封装生成pdf文件的共用配置3.生成pdf文件的文件模板内容4.调用方法生成pdf 利用pdfmake生成pdf文件 1.下载安装pdfmake第三方包 npm i pdfma