linux的i2c子系统
- linux构建的2c驱动框架十分庞大,完全讲透驱动框架,需要代码和理论的深入理解,在此不过多展开,很多博客讲解十分透彻。
- 记住四大块i2c的总线、设备、驱动、适配器,总线和适配器是厂家和内核提供好的,设备和驱动是由开发者进行编写的。
- 设备 - 由设备树中信息过度而来,最终驱动之中以struct i2c_client数据结构体的方式呈现
- 驱动 - 和platform驱动类似,提供probe钩子函数进行开发等常规操作。
- 下面就可以就开始进行sht20 i2c驱动开发。
设备树
- 在自己构建的设备树或者官方设备树文件中i2c1添加sht20设备节点,前提内核开启了i2c驱动支持
- 追加的代码如下
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
sht20-i2c@40 {
compatible = "ssr,sht20";
reg = <0x40>;
};
};
- 然后加载设备树后,查看开发板的/sys下是否有i2c设备添加
驱动代码部分
驱动描述
- 该部分和platform驱动十分类似,compatible关键字进行匹配
- 代码如下:
//设备数匹配列表
static const struct of_device_id of_sht20_match [] = {
{.compatible="ssr,sht20"},
{}
};
MODULE_DEVICE_TABLE(of, of_sht20_match);
static struct i2c_driver sht20_drv = {
.probe = sht20_probe,
.remove = sht20_remove,
.driver = {
.name = "sht20 driver 0.1", //name 不重要
.owner = THIS_MODULE,
.of_match_table = of_sht20_match,
},
.id_table = sht20_id,
};
probe函数
- 驱动安装后,与设备进行匹配后执行的函数
- 依然是常规操作,不过引入一个sysfs调试方法,步骤如下
1、设备号创建
2、注册字符设备
3、创建类
4、创建设备
5、创建sys属性
6、保存私有数据 - 代码如下:
/* @description: i2c驱动probe函数,设备和驱动匹配后执行
*
* @parm : client - i2c设备
* @parm : id - i2c设备ID
* @return: 0 successfully , !0 failure
*/
static int sht20_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct sht20_priv *priv = NULL;
//struct device *dev;
dev_t devno; //设备号
int rv = 0;
//0.给priv分配空间
priv = devm_kzalloc(&client->dev, sizeof(struct sht20_priv), GFP_KERNEL);
if(!priv)
{
return -ENOMEM;
}
//1.创建设备号
if(0 != dev_major)
{
devno = MKDEV(dev_major, 0);
rv = register_chrdev_region(devno, 1, DEV_NAME); //静态创建
}
else
{
rv = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);//动态创建
dev_major = MAJOR(devno);//获主设备号
}
if(rv < 0)
{
dev_err(&client->dev, "%s driver can't get major %d\n", DEV_NAME, dev_major);
return rv;
}
//2.注册字符设备
cdev_init(&priv->cdev, &sht20_fops); //初始化cdev
priv->cdev.owner = THIS_MODULE;
rv = cdev_add(&priv->cdev, devno, 1);
if(0 != rv)
{
dev_err(&client->dev, "error %d add %s device failure.\n", rv, DEV_NAME);
goto undo_major;
}
//3.创建类,驱动进行节点创建
priv->dev_class = class_create(THIS_MODULE, DEV_NAME);
if(IS_ERR(priv->dev_class))
{
dev_err(&client->dev, "%s driver create class failure.\n", DEV_NAME);
rv = -ENOMEM;
goto undo_cdev;
}
//4.创建设备
priv->dev = device_create(priv->dev_class, NULL, devno, NULL, DEV_NAME);
if(IS_ERR(priv->dev))
{
rv = -ENOMEM;
goto undo_class;
}
//5. 创建sys 属性 在platform下
if(device_create_file(priv->dev, &dev_attr_sht20_temp))
{
rv = -ENOMEM;
goto undo_device;
}
//6. 保存私有数据
priv->client = client;
i2c_set_clientdata(client, priv);
dev_set_drvdata(priv->dev, priv);
dev_info(&client->dev, "sht20 i2c driver probe okay.\n");
return 0;
undo_device:
device_destroy(priv->dev_class, devno);
undo_class:
class_destroy(priv->dev_class);
undo_cdev:
cdev_del(&priv->cdev);
undo_major:
unregister_chrdev_region(devno, 1);
devm_kfree(&client->dev, priv);
return rv;
}
fops操作
open函数
- 进行私有设备数据地址的查找
- 进行sht20的初始化
/* @description: sht20 打开设备
*
* @parm : inode - 传递给驱动的inode
* @parm : filp - 设备文件,利用其私有数据成员
* @return: 0 successfully , !0 failure
*/
static int sht20_open(struct inode *inode, struct file *filp)
{
struct sht20_priv *priv = container_of(inode->i_cdev, struct sht20_priv, cdev);
int rv;
//初始化sht20
rv = sht20_init(priv->client);
if(rv < 0)
{
dev_err(priv->dev, "sht20 init failure.\n");
}
//dev_info(priv->dev, "sht20 init successfully.\n");
filp->private_data = priv;
return 0;
}
read接口
- 准备工作,把读取温湿度的功能进行函数封装,在功能函数中调用适配器提供的传输算法实现底层的接口
- 具体传输的数据根据数据手册来,相应的命令和收到数据的规格,都可以由数据手册查询得到
- 主要将获取的温湿度数据进行倍数放大后,传递给用户buf中
- 代码如下
/* @description: 从设备读取文件
*
* @parm : filp - 设备文件,文件描述符
* @parm : buf - 返回给用户空间的数据缓冲区
* @parm : cnt - 要读取的数据长度
* @parm : offt - 相对于文件首地址的偏移
* @return: 读取的字节数,负数 - 读取失败
*/
static ssize_t sht20_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
int rv = 0;
//struct i2c_client *client = filp->private_data;
struct sht20_priv *priv = filp->private_data;
unsigned char data[4] = {0,};
if(!priv->client)
{
printk("failure to get i2c_client.\n");
return -EFAULT;
}
rv = read_temperature(priv->client, data);
if(rv)
{
dev_err(priv->dev, "read_temperature failure.\n");
}
data[3] = data[1];
data[2] = data[0];
rv = read_humidity(priv->client, data);
if(rv)
{
dev_err(priv->dev, "read_humidity failure.\n");
}
//printk("test %x %x %x %X \n", data[3], data[2], data[1], data[0]);
rv = copy_to_user(buf, data, sizeof(data));
if(rv)
{
dev_err(priv->dev, "copy to user error.\n");
return -EFAULT;
}
return sizeof(data);
}
sysfs功能
- 作为调试技术的一部分,把数据直接通过sysfs文件系统,呈现给用户空间,加速调试
- 使用步骤如下
1、属性定义
2、编写属性读写回调函数
3、创建文件并与属性结合 - 本文使用简单创建方式,单属性创建,使用设备提供的文件创建api
- 代码如下
static ssize_t sht20_temp_show(struct device *dev, struct device_attribute *attr, char *buf)
{
struct sht20_priv *priv = dev_get_drvdata(dev);
int rv = 0;
unsigned char data[4] = {0,};
if(!priv->client)
{
printk("failure to get i2c_client.\n");
return -EFAULT;
}
rv = read_temperature(priv->client, data);
if(rv)
{
dev_err(priv->dev, "read_temperature failure.\n");
}
data[3] = data[1];
data[2] = data[0];
rv = read_humidity(priv->client, data);
if(rv)
{
dev_err(priv->dev, "read_humidity failure.\n");
}
//printk("test %x %x %x %X \n", data[3], data[2], data[1], data[0]);
return sprintf(buf, "temperature=%d humidity=%d\n", ((data[3] << 8) | data[2]), ((data[1] << 8) | data[0]) );//1000倍
}
static ssize_t sht20_temp_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
char k_buf[10] = {0,};
snprintf(k_buf, sizeof(k_buf), "%s", buf);
dev_info(dev, "Don't echo to me -> [%s] size [%d]\n", k_buf, count);
return count;
}
static DEVICE_ATTR(sht20_temp, 0644, sht20_temp_show, sht20_temp_store);
- 创建过程在probe之中
- 写属性的接口函数,由于不是主要功能,只做了数据回显功能
- 读属性中进行温湿度的采样
驱动完整代码
完整代码如下
/*********************************************************************************
* Copyright: (C) 2022 weihh
* All rights reserved.
*
* Filename: sht20_i2c_drv.c
* Description: This file is sht20 driver code.
*
* Version: 1.0.0(2022年04月17日)
* Author: Wei Huihong <[email protected]>
* ChangeLog: 1, Release initial version on "2022年04月17日 21时34分06秒"
*
********************************************************************************/
#include <linux/module.h> //所有模块都需要的头文件
#include <linux/init.h> // init和exit相关宏
#include <linux/kernel.h> // printk(),内核打印函数
#include <linux/delay.h> // 延时函数头文件
#include <linux/device.h> // 用于设备创建的函数头文件
#include <linux/fs.h> //和fops相关的头文件
#include <linux/cdev.h> //字符设备 初始化相关
#include <linux/version.h>
#include <linux/ide.h>
#include <linux/gpio.h> //gpio子系统头文件
#include <linux/of_gpio.h> //gpio子系统和设备树相关
#include <linux/platform_device.h> //platform总线设备相关
#include <linux/err.h> //错误码相关
#include <linux/timer.h> //定时器相关
#include <linux/i2c.h> //i2c子系统相关
#define DEV_NAME "sht20" //设备名
#define SHT20_SOFERESET 0xFE // 软复位
#define SHT20_TEMPERATURE_NO_HOLD_CMD 0xF3 // 无主机模式触发温度测量
#define SHT20_HUMIDITY_NO_HOLD_CMD 0xF5 // 无主机模式触发湿度测量
#define SHT20_TEMPERATURE_HOLD_CMD 0xE3 // 主机模式触发温度测量
#define SHT20_HUMIDITY_HOLD_CMD 0xE5 // 主机模式触发湿度测量
#define CRC_MODEL 0x131
#define CRC_SUCCESS 0
#define CRC_FAIL 1
#ifndef DEV_MAJOR
#define DEV_MAJOR 0
#endif
static int dev_major = DEV_MAJOR; //主机号
struct sht20_priv {
struct cdev cdev;
struct class *dev_class;
struct i2c_client *client;
struct device *dev;
};
/* @description: 初始化sht20
*
* @parm : client - i2c 设备
* @parm :
* @return: 0 successfully , !0 failure
*/
static int sht20_init(struct i2c_client *client)
{
int rv;
char data = SHT20_SOFERESET;
rv = i2c_master_send(client, &data, 1);
if(rv < 0)
{
dev_err(&client->dev, "i2c send init cmd failure.\n");
return -1;
}
msleep(50);
return 0;
}
static int crc_check(unsigned char *data, int len, unsigned char checksum)
{
unsigned char crc = 0x00;
int i, j;
for(i=0; i<len; i++)
{
crc ^= *data++;
for (j=0; j<8; j++)
{
if (crc & 0x80)
{
crc = (crc << 1) ^ CRC_MODEL;
}
else
{
crc = (crc << 1);
}
}
}
// printk("crc clu data : [%x]\n", crc);
if(checksum == crc)
{
return CRC_SUCCESS;
}
else
{
return CRC_FAIL;
}
}
/* @description: 读取sht20 的温度数据
*
* @parm : client - i2c 设备
* @parm : buf - 存储读取的数据
* @return: 0 successfully , !0 failure
*/
static int read_temperature(struct i2c_client *client, unsigned char *buf)
{
int rv = 0;
int temperature = 0;
unsigned char tmp[3] = {0};
char data = SHT20_TEMPERATURE_HOLD_CMD;
//形参判断
if(!client || !buf)
{
printk("%s line [%d] %s() get invalid input arguments\n", __FILE__, __LINE__, __func__ );
return -1;
}
//发送CMD
rv = i2c_master_send(client, &data, 1);
//rv = i2c_smbus_write_byte(client, SHT20_TEMPERATURE_NO_HOLD_CMD);
if(rv < 0)
{
dev_err(&client->dev, "i2c send tmper cmd failure.\n");
return -1;
}
//delay 85ms
msleep(85);
//读取数据
rv = i2c_master_recv(client, tmp, sizeof(tmp));
if(rv < 0)
{
dev_err(&client->dev, "i2c recv tmper data failure.\n");
return -1;
}
// printk("read temperature: tmp[0] %x tmp[1] %x ; crc : tmp[2] %x\n", tmp[0], tmp[1], tmp[2]); //验证 crc校验结果
//数据处理
temperature = (tmp[0] << 8) | (tmp[1]&0xFC);
temperature = ((temperature * 175720) >> 16) - 46850;
//printk("temperature : %d\n", temperature);
//TODO: 可以加上CRC校验
if(0 != crc_check(tmp, 2, tmp[2]))
{
dev_err(&client->dev, "tmperature data fails to pass cyclic redundancy check\n");
return -1;
}
buf[0] = temperature & 0xFF;
buf[1] = (temperature >> 8) & 0xFF;
return 0;
}
/* @description: 读取sht20 的相对数据数据
*
* @parm : client - i2c 设备/客户端
* @parm : buf - 存储读取的数据
* @return: 0 successfully , !0 failure
*/
static int read_humidity(struct i2c_client *client, unsigned char *buf)
{
int rv = 0;
int humidity = 0;
unsigned char tmp[3] = {0};
char data = SHT20_HUMIDITY_HOLD_CMD;
//形参判断
if(!client || !buf)
{
printk("%s line [%d] %s() get invalid input arguments\n", __FILE__, __LINE__, __func__ );
return -1;
}
//发送CMD
rv = i2c_master_send(client, &data, 1);
//rv = i2c_smbus_write_byte(client, SHT20_HUMIDITY_NO_HOLD_CMD);
if(rv < 0)
{
dev_err(&client->dev, "i2c send humidity cmd failure.\n");
return -1;
}
//delay 29ms
msleep(29);
//读取数据
rv = i2c_master_recv(client, tmp, sizeof(tmp));
if(rv < 0)
{
dev_err(&client->dev, "i2c recv humidity data failure.\n");
return -1;
}
// printk("read humidity: tmp[0] %x tmp[1] %x ; crc : tmp[2] %x\n", tmp[0], tmp[1], tmp[2]);
//数据处理
humidity = (tmp[0] << 8) | (tmp[1]&0xFC);
humidity = ((humidity * 125000) >> 16) - 6000;
//crc-8 校验
if(0 != crc_check(tmp, 2, tmp[2]))
{
dev_err(&client->dev, "tmperature data fails to pass cyclic redundancy check\n");
return -1;
}
buf[0] = humidity & 0xFF;
buf[1] = (humidity >> 8) & 0xFF;
return 0;
}
/* @description: sht20 打开设备
*
* @parm : inode - 传递给驱动的inode
* @parm : filp - 设备文件,利用其私有数据成员
* @return: 0 successfully , !0 failure
*/
static int sht20_open(struct inode *inode, struct file *filp)
{
struct sht20_priv *priv = container_of(inode->i_cdev, struct sht20_priv, cdev);
int rv;
//初始化sht20
rv = sht20_init(priv->client);
if(rv < 0)
{
dev_err(priv->dev, "sht20 init failure.\n");
}
//dev_info(priv->dev, "sht20 init successfully.\n");
filp->private_data = priv;
return 0;
}
/* @description: 从设备读取文件
*
* @parm : filp - 设备文件,文件描述符
* @parm : buf - 返回给用户空间的数据缓冲区
* @parm : cnt - 要读取的数据长度
* @parm : offt - 相对于文件首地址的偏移
* @return: 读取的字节数,负数 - 读取失败
*/
static ssize_t sht20_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
int rv = 0;
//struct i2c_client *client = filp->private_data;
struct sht20_priv *priv = filp->private_data;
unsigned char data[4] = {0,};
if(!priv->client)
{
printk("failure to get i2c_client.\n");
return -EFAULT;
}
rv = read_temperature(priv->client, data);
if(rv)
{
dev_err(priv->dev, "read_temperature failure.\n");
}
data[3] = data[1];
data[2] = data[0];
rv = read_humidity(priv->client, data);
if(rv)
{
dev_err(priv->dev, "read_humidity failure.\n");
}
//printk("test %x %x %x %X \n", data[3], data[2], data[1], data[0]);
rv = copy_to_user(buf, data, sizeof(data));
if(rv)
{
dev_err(priv->dev, "copy to user error.\n");
return -EFAULT;
}
return sizeof(data);
}
/* @description: 关闭设备
*
* @parm : inode - 传递给驱动的inode
* @parm : filp - 设备文件,file结构体有个私有数据区可以使用
* @return: 0 successfully , !0 failure
*/
static int sht20_release(struct inode *inode, struct file *filp)
{
return 0;
}
//设备操作函数
static struct file_operations sht20_fops = {
.owner = THIS_MODULE,
.open = sht20_open,
.read = sht20_read,
.release = sht20_release,
};
/* @description: sysfs - 温度属性显示函数
*
* @parm : dev - 设备指针,创建file时候会指定dev
* @parm : attr - 设备属性,创建时候传入
* @parm : buf - 传出给sysfs中显示的buf
* @return: 显示的字节数
* @TODO: 函数不够正规,了解PAGE_SIZE
*/
static ssize_t sht20_temp_humi_show(struct device *dev, struct device_attribute *attr, char *buf)
{
struct sht20_priv *priv = dev_get_drvdata(dev);
int rv = 0;
unsigned char data[4] = {0,};
if(!priv->client)
{
printk("failure to get i2c_client.\n");
return -EFAULT;
}
rv = read_temperature(priv->client, data);
if(rv)
{
dev_err(priv->dev, "read_temperature failure.\n");
}
data[3] = data[1];
data[2] = data[0];
rv = read_humidity(priv->client, data);
if(rv)
{
dev_err(priv->dev, "read_humidity failure.\n");
}
//printk("test %x %x %x %X \n", data[3], data[2], data[1], data[0]);
return sprintf(buf, "temperature=%d humidity=%d\n", ((data[3] << 8) | data[2]), ((data[1] << 8) | data[0]) );//1000倍
}
/* @description: sysfs - echo写入属性函数
*
* @parm : dev - 设备指针,创建file时候会指定dev
* @parm : attr - 设备属性,创建时候传入
* @parm : buf - 用户空间的buf
* @parm : count - 传入buf的size
* @return: 写入的buf大小
*/
static ssize_t sht20_temp_humi_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
char k_buf[10] = {0,};
snprintf(k_buf, sizeof(k_buf), "%s", buf);
dev_info(dev, "Don't echo to me -> [%s] size [%d]\n", k_buf, count);
return count;
}
//初始化属性值
static DEVICE_ATTR(sht20_temp_humi, 0644, sht20_temp_humi_show, sht20_temp_humi_store);
/* @description: i2c驱动probe函数,设备和驱动匹配后执行
*
* @parm : client - i2c设备
* @parm : id - i2c设备ID
* @return: 0 successfully , !0 failure
*/
static int sht20_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct sht20_priv *priv = NULL;
//struct device *dev;
dev_t devno; //设备号
int rv = 0;
//0.给priv分配空间
priv = devm_kzalloc(&client->dev, sizeof(struct sht20_priv), GFP_KERNEL);
if(!priv)
{
return -ENOMEM;
}
//1.创建设备号
if(0 != dev_major)
{
devno = MKDEV(dev_major, 0);
rv = register_chrdev_region(devno, 1, DEV_NAME); //静态创建
}
else
{
rv = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);//动态创建
dev_major = MAJOR(devno);//获主设备号
}
if(rv < 0)
{
dev_err(&client->dev, "%s driver can't get major %d\n", DEV_NAME, dev_major);
return rv;
}
//2.注册字符设备
cdev_init(&priv->cdev, &sht20_fops); //初始化cdev
priv->cdev.owner = THIS_MODULE;
rv = cdev_add(&priv->cdev, devno, 1);
if(0 != rv)
{
dev_err(&client->dev, "error %d add %s device failure.\n", rv, DEV_NAME);
goto undo_major;
}
//3.创建类,驱动进行节点创建
priv->dev_class = class_create(THIS_MODULE, DEV_NAME);
if(IS_ERR(priv->dev_class))
{
dev_err(&client->dev, "%s driver create class failure.\n", DEV_NAME);
rv = -ENOMEM;
goto undo_cdev;
}
//4.创建设备
priv->dev = device_create(priv->dev_class, NULL, devno, NULL, DEV_NAME);
if(IS_ERR(priv->dev))
{
rv = -ENOMEM;
goto undo_class;
}
//5. 创建sys 属性 在platform下
if(device_create_file(priv->dev, &dev_attr_sht20_temp_humi))
{
rv = -ENOMEM;
goto undo_device;
}
//6. 保存私有数据
priv->client = client;
i2c_set_clientdata(client, priv);
dev_set_drvdata(priv->dev, priv);
dev_info(&client->dev, "sht20 i2c driver probe okay.\n");
return 0;
undo_device:
device_destroy(priv->dev_class, devno);
undo_class:
class_destroy(priv->dev_class);
undo_cdev:
cdev_del(&priv->cdev);
undo_major:
unregister_chrdev_region(devno, 1);
devm_kfree(&client->dev, priv);
return rv;
}
/* @description: i2c驱动的remove函数,移除时候执行
*
* @parm : client - i2c
* @parm :
* @return: 0 successfully , !0 failure
*/
static int sht20_remove(struct i2c_client *client)
{
struct sht20_priv *priv = i2c_get_clientdata(client);
dev_t devno = MKDEV(dev_major, 0);
//删除sys中的属性
device_remove_file(priv->dev, &dev_attr_sht20_temp_humi);
//设备销毁
device_destroy(priv->dev_class, devno);
//注销类
class_destroy(priv->dev_class);
//删除字符设备
cdev_del(&priv->cdev);
unregister_chrdev_region(devno, 1);
//释放堆
devm_kfree(&client->dev, priv);
dev_info(&client->dev, "sht20 driver remove.\n");
return 0;
}
//传统方式ID列表
static const struct i2c_device_id sht20_id[] = {
{"ssr,sht20", 0},
{}
};
//设备树匹配列表
static const struct of_device_id of_sht20_match [] = {
{.compatible="ssr,sht20"},
{}
};
MODULE_DEVICE_TABLE(of, of_sht20_match);
static struct i2c_driver sht20_drv = {
.probe = sht20_probe,
.remove = sht20_remove,
.driver = {
.name = "sht20 driver 0.1", //name 不重要
.owner = THIS_MODULE,
.of_match_table = of_sht20_match,
},
.id_table = sht20_id,
};
//注册sht20驱动
module_i2c_driver(sht20_drv);
MODULE_AUTHOR("Wei Huihong <[email protected]>");
MODULE_DESCRIPTION("i.MX6ULL sht20 driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("i2c:imx_sht20_i2c_driver");
测试
- app即为使用open、read系统调用测试
- 代码如下
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEV_STH20 "/dev/sht20"
int main(int argc, char *argv[])
{
float temperature = 0.0;
float humidity = 0.0;
unsigned char data[4] = {0,};
int rv = -1;
int fd_sht20 = -1;
fd_sht20 = open(DEV_STH20, O_RDONLY);
if(fd_sht20 < 0)
{
printf("open %s failure.\n", DEV_STH20);
return -1;
}
printf("open %s successful.\n", DEV_STH20);
while(1)
{
//printf("start read temperature and humidity.\n");
memset(data, 0, sizeof(data));
rv = read(fd_sht20, data, sizeof(data));
if(rv < 0)
{
printf("read data failure.\n");
}
else
{
//printf("data[0-3] : %x %x %x %x\n",data[0], data[1], data[2], data[3]);
humidity = ((data[1] << 8) | data[0]) / 1000.0;
temperature = ((data[3] << 8) | data[2]) /1000.0;
printf("temperature : %.4f℃ humidity : %.4f %%\n", temperature, humidity);
}
sleep(5);
}
close(fd_sht20);
return 0;
}
- cat 属性 即为sysfs调试方式
- 效果图如下
- 误差在接受范围以内