Linux驱动基础篇(一)GPIO(上)LED驱动

Linux驱动基础(一)GPIO(上)LED驱动

一、开发环境准备

ubuntu22.04 + Vscode + OrangePi+

1.安装交叉编译工具+编译内核

(1)安装交叉编译工具

📌选择交叉编译工具链

以官网下载页面为例

在这里插入图片描述

在这里插入图片描述

📌下载交叉编译工具

交叉编译工具链官方下载地址(国外)
交叉编译工具链清华开源镜像站下载地址(国内)

📌解压、添加环境变量、测试安装结果

sudo tar -xvf arm-none-linux-gnueabihf-12.3-x86_64-.tar.xz -C /mnt	# 解压到/mnt目录
# 添加环境变量,在原有的双引号前输入:后粘贴复制工具链的路径
# 例如这里是/mnt/arm-none-linux-gnueabihf-12.3-x86_64/bin
# 保存后重新登录或重启生效,输入arm-尝试tab能否补全命令以及arm-none-linux-gnueabihf-gcc -v进行测试
sudo vi /etc/environment
(2)修改Makefile指定编译器和架构
# 在内核源码顶层目录的Makefile,搜索CROSS_COMPILE或ARCH,添加以下内容
# 要配置上面步骤后,CROSS_COMPILE可以这样写;没有的话要写绝对路径
ARCH  := arm
CROSS_COMPILE	:= arm-none-linux-gnueabihf-
(3)生成配置文件.config
# 生成默认的配置文件,也可以make menuconfig进行配置,最后会保存到.config
make defconfig

❓❓❓ 报错1:arch/arm是一个目录 ❓❓❓

解决:ARCH := arm (arm后面多了空格)

❓❓❓ 报错2:/bin/sh: 1: flex: not found ❓❓❓

sudo apt-get install flex

❓❓❓ 报错3:/bin/sh: 1: bison: not found ❓❓❓

sudo apt-get install bison

❓❓❓ 报错4:make menuconfig 打开失败❓❓❓

sudo apt-get install libncurses5-dev


(4)编译内核
# 建议这里可以将所有的CPU核心分配给虚拟机,提高速度
make -j24

❓❓❓ 报错5:scripts/extract-cert.c:21:10: fatal error: openssl/bio.h: 没有那个文件或目录❓❓❓

原因:没有安装libssl-dev或者安装版本过高
解决:(未安装)sudo apt-get install libssl-dev
(版本过高)sudo apt-get install aptitude(安装aptitude软件包管理器)
sudo aptitude install libssl-dev(使用aptitude安装libssl-dev)
选择不保持当前版本,出现提示输出n,确认y后降级


2.安装配置vscode

二、第一个驱动程序HelloWorld

1.简单的驱动框架使用

简单的驱动框架分为三步:①装载驱动、②操作驱动、③卸载驱动。个人认为这样划分容易理解和记忆!
在这里插入图片描述

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/err.h>

#define   DEV_NAME   "hello"

static struct class *hello_cls;
static int ERR;
static struct device *device;

static int hello_open(struct inode *inode, struct file *file){
    printk("%s  %s  %d\n",__FILE__,__FUNCTION__ ,__LINE__);
    return 0;
}
static ssize_t hello_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
    printk("%s  %s  %d\n",__FILE__,__FUNCTION__ ,__LINE__);
    
    return 0;
}

static ssize_t hello_write (struct file *file, const char __user *buf, size_t size, loff_t * offset){
    printk("%s  %s  %d\n",__FILE__,__FUNCTION__ ,__LINE__);
    return 0;
}
static int hello_release(struct inode *inode, struct file *file){
    printk("%s  %s  %d\n",__FILE__,__FUNCTION__ ,__LINE__);
    return 0;
}

static unsigned int major = 0;
static struct file_operations hello_fop = {
        .owner = THIS_MODULE,
        .open = hello_open,
        .read = hello_read,
        .write = hello_write,
        .release = hello_release
};

/**
 * @brief 初始化驱动(注册设备register_chrdev()、创建设备节点device_create())
 * 注册设备需要:设备号、设备名、设备file_operaction
 * 创建设备节点需要:class、devtype-由设备号确定、设备节点名
 **/
static int  __init hello_Driver_Init(void){
    printk("%s  %s  %d\n",__FILE__,__FUNCTION__ ,__LINE__);
    major = register_chrdev(major,DEV_NAME,&hello_fop);
    hello_cls = class_create(THIS_MODULE,DEV_NAME);
    if(IS_ERR(hello_cls)){
        ERR = PTR_ERR(hello_cls);
        pr_err("class create failed.(error code:%d)\n",ERR);
        unregister_chrdev(major,DEV_NAME);
        return ERR;
    }
    device = device_create(hello_cls,NULL, MKDEV(major,0),NULL,"hello");
    if(IS_ERR(device)){
        ERR = PTR_ERR(device);
        pr_err("device create failed.(error code:%d)\n",ERR);
        class_destroy(hello_cls);
        unregister_chrdev(major,DEV_NAME);
        return ERR;
    }
    return 1;
}

/**
 * @brief 卸载驱动(删除设备节点device_destroy()、注销设备unregister_chrdev())
 **/
static void __exit hello_Driver_Exit(void){
    printk("%s  %s  %d\n",__FILE__,__FUNCTION__ ,__LINE__);
    device_destroy(hello_cls, MKDEV(major,0));
    class_destroy(hello_cls);
    unregister_chrdev(major,DEV_NAME);
}

module_init(hello_Driver_Init);
module_exit(hello_Driver_Exit);
MODULE_LICENSE("GPL");
2.重要的几个宏/函数

2.1 错误指针判断IS_ERR( )、PTR_ERR( )和pr_err( )

2.2 用户和内核之间的数据拷贝copy_from_user( )、copy_to_user( )和get_user( )、put_user( )

三、第二个驱动程序点亮LED

1.阅读原理图和芯片手册

(1)找到OrangePi PC+的两个LED

📌在原理图中搜索"LED",找到关于OrangePi+ LED部分的原理图

在这里插入图片描述

  • 从原理图关于LED的部分可以看到,OrangePi PC+有两个LED:PWR-LED(供电时闪烁LED)、STATUS-LED(开机运行时状态LED)。
(2)找到两个LED和CPU连接的引脚

在这里插入图片描述

  • PWR-LED连接到PL10、STATUS_LED连接到PA15
(3)在芯片手册找到PL10和PA15

在这里插入图片描述

(4)找到对应的配置寄存器

在这里插入图片描述

(5)找到对应的数据寄存器

在这里插入图片描述

2.从驱动开始点亮香橙派orangepi plus的LED

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/err.h>
#include <asm/io.h>
#include <linux/types.h>
#include <linux/uaccess.h>

#define PIO_BASE    0x01c20800

static volatile unsigned int *PA_CFG1_REG;
static volatile unsigned int *PA_DATA_REG;
static unsigned int major = 0;
static struct class *led_class;

static int led_open(struct inode *inode, struct file *file){
    /* output: bit30-28 = 001 */
    *PA_CFG1_REG &= ~(0x01 << 30);
    *PA_CFG1_REG &= ~(0x01 << 29);
    *PA_CFG1_REG |= (0x01 << 28);
    printk("======= led_open():PA_CFG_REG = 0x%x =========\n",*PA_CFG1_REG);
    return 0;
}
static int led_close(struct inode *inode, struct file *file){
    /* default: bit30-28 = 111 */
    *PA_CFG1_REG |= (0x01 << 30);
    *PA_CFG1_REG |= (0x01 << 29);
    *PA_CFG1_REG |= (0x01 << 28);
    printk("======= led_close():PA_CFG_REG = 0x%x =========\n",*PA_CFG1_REG);
    return 0;
}
static ssize_t led_read (struct file *file, char __user *buf, size_t size, loff_t *off){
    return 0;
}
static ssize_t led_write (struct file *file, const char __user *buf, size_t size, loff_t *off){
    char val;   // 0-OFF 1-ON
    int ret = copy_from_user(&val,buf,1);
    ret = 3;
    if(val){
        *PA_DATA_REG |= 0x1<<15; //bit15 = 1
    }else{
        *PA_DATA_REG &= ~(0x1<<15); //bit15 = 0
    }
    return 1;
}
static struct file_operations led_ops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_close,
};
static int  __init led_driver_init(void){
    printk("======= %s %s %d ========\n",__FILE__,__FUNCTION__ ,__LINE__);
    major = register_chrdev(major,"led_driver",&led_ops);
    led_class = class_create(THIS_MODULE,"led_driver");
    device_create(led_class,NULL, MKDEV(major,0),NULL,"led1");
    PA_CFG1_REG = ioremap(PIO_BASE+0x04,4);
    PA_DATA_REG = ioremap(PIO_BASE+0x10,4);
    *PA_CFG1_REG &= ~(0x1<<30);
    *PA_CFG1_REG &= ~(0x1<<29);
    *PA_CFG1_REG |= 0x1<<28;
    return 0;
}

static void  __exit led_driver_exit(void){
    printk("======= %s %s %d ========\n",__FILE__,__FUNCTION__ ,__LINE__);
    iounmap(PA_CFG1_REG);
    iounmap(PA_DATA_REG);
    device_destroy(led_class, MKDEV(major,0));
    class_destroy(led_class);
    unregister_chrdev(major,"led_driver");

}

module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");

这里为了没有写应用层的测试程序,只简单粗暴点个LED,加载驱动后led亮,香橙派OrangePi Plus上是红灯

四、面向对象设计思想

上面所写的LED驱动程序,驱动和硬件操作绑定在一起,更换开发板平台所有的代码就完全不适用,不仅可维护性和可读性差,而且代码难以移植到其他平台,增加了代码冗余和维护的工作量。因此,内核驱动程序中采用面向对象的编程思想,将驱动和硬件操作的代码分离并进行合理的抽象和封装,通常是更好的选择

📌改写LED驱动,实现应用层程序控制两个LED亮灭

【led_test_app.c】

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

/** ./test_led_app /dev/led1 on **/
/** ./test_led_app /dev/led2 off **/

int main(int argc,char **argv){
    char *buf = (char *)malloc(36);
    int nwrite = 0;
    if(argc < 3){
        printf("Usage:%s [dev-led1/led2]  [status-on/off]  \n",argv[0]);
        return -1;
    }
    int fd = open(argv[1],O_RDWR);
    if(fd < 0){
        printf("open device error\n");
        return -1;
    }

    int val_on = 1;
    int val_off = 0;

    if( 0 == strcmp(argv[2],"on")){
        nwrite = write(fd,&val_on,sizeof(val_on));
    }else if( 0 == strcmp(argv[2],"off")){
        nwrite = write(fd,&val_off,sizeof(val_off));
    }else{
        printf("syntax error\n");
    }
    close(fd);
    return 0;
}

【orangepi_plus_leds.h】

#ifndef _ORANGEPI_PLUS_LEDS_H
#define _ORANGEPI_PLUS_LEDS_H

#define MIN(a,b)    (a < b ? a : b)
#define PIN_GROUP(pin)  (pin >> 22)
#define PIN_NUM(pin)    (pin & 0x3FFFFF)

struct GPIOx_PIN {   
    /** bit[21:0] pin_num **/
    /** bit[31:22] pin_group **/
    int pin; 
    int count;
    volatile unsigned int *GPIOx_CFG_REG;        //配置寄存器
    volatile unsigned int *GPIOx_DAT_REG;        //数据寄存器
    int (*gpio_init)(int pin);             //初始化函数
    int (*gpio_control)(int pin,int status);   //控制函数
    struct GPIOx_PIN *next;
};

extern struct GPIOx_PIN *get_GPIOA_PIN15(void);
extern struct GPIOx_PIN *get_GPIOL_PIN10(void);

#endif

【orangepi_plus_led1.c】

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/err.h>
#include <asm/io.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include "orangepi_plus_leds.h"

#define BASE_ADDR   (0x01c20800)

#define WHICH_CFG(pin_num)  \
	((pin_num >= 0 && pin_num <= 7)   ?  0x00 : \
	 (pin_num >= 8 && pin_num <= 15)  ?  0x04 : \
	 (pin_num >= 15 && pin_num <= 21) ?  0x08 : 0x0c)


int led1_init(int pin);
int led1_control(int pin,int status);

static struct GPIOx_PIN GPIOA_PIN15 = {
	.pin = (0 << 22)|(15),          //第0组第15pin(GPIOA_15)
	.gpio_init = led1_init,
	.gpio_control = led1_control,
	.next = NULL
};

int led1_init(int pin){
	int pin_num = PIN_NUM(pin);
	GPIOA_PIN15.GPIOx_CFG_REG = ioremap(BASE_ADDR + PIN_GROUP(pin)* 0x24 + WHICH_CFG(pin_num),4);
	GPIOA_PIN15.GPIOx_DAT_REG = ioremap(BASE_ADDR + PIN_GROUP(pin)* 0x24 + 0x10,4);
	if(GPIOA_PIN15.GPIOx_CFG_REG == NULL || GPIOA_PIN15.GPIOx_DAT_REG == NULL){	
		pr_err("======== %s:ioremap address error =======\n",__FUNCTION__);
		return -1;
	}
	*(GPIOA_PIN15.GPIOx_CFG_REG) &= ~(0x1<<29);
	*(GPIOA_PIN15.GPIOx_CFG_REG) &= ~(0x1<<30);
	*(GPIOA_PIN15.GPIOx_CFG_REG) |= (0x1<<28);
	return 0;
}

int led1_control(int pin,int status){   //status 0-off 1-on
	switch(status){
		case 0: //off
			*(GPIOA_PIN15.GPIOx_DAT_REG) &= ~(0x1 << PIN_NUM(pin));
			break;
		case 1: //on
			*(GPIOA_PIN15.GPIOx_DAT_REG) |= (0x1 << PIN_NUM(pin));
			break;
		default:
			break;
	}
	return 0;
}

struct GPIOx_PIN *get_GPIOA_PIN15(void){
	return &GPIOA_PIN15;
}

EXPORT_SYMBOL(get_GPIOL_PIN10);
MODULE_LICENSE("GPL");

【orangepi_plus_led2.c】

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/err.h>
#include <asm/io.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include "orangepi_plus_leds.h"

#define BASE_ADDR   (0x01f02c00)

#define WHICH_CFG(pin_num)  \
        ((pin_num >= 0 && pin_num <= 7)   ?  0x00 : \
         (pin_num >= 8 && pin_num <= 15)  ?  0x04 : \
         (pin_num >= 15 && pin_num <= 21) ?  0x08 : 0x0c)

int led2_init(int pin);
int led2_control(int pin,int status);

static struct GPIOx_PIN GPIOL_PIN10 = {
    .pin = (0 << 22)|(10),         //第0组第10pin(GPIOL_10)
    .gpio_init = led2_init,
    .gpio_control = led2_control,
    .next = NULL
};


int led2_init(int pin){
    int pin_num = PIN_NUM(pin);
    GPIOL_PIN10.GPIOx_CFG_REG = ioremap(BASE_ADDR + PIN_GROUP(pin)*0x24 + WHICH_CFG(pin_num),4);
    GPIOL_PIN10.GPIOx_DAT_REG = ioremap(BASE_ADDR + PIN_GROUP(pin)*0x24 + 0x10,4);
    if(GPIOL_PIN10.GPIOx_CFG_REG == NULL || GPIOL_PIN10.GPIOx_DAT_REG == NULL){
         pr_err("======= %s ioremap address error ========\n",__FUNCTION__);
         return -1;
     }
    *(GPIOL_PIN10.GPIOx_CFG_REG) &= ~(0x1<<29);
    *(GPIOL_PIN10.GPIOx_CFG_REG) &= ~(0x1<<30);
    *(GPIOL_PIN10.GPIOx_CFG_REG) |= (0x1<<28);
    return 0;
}

int led2_control(int pin,int status){   //status 0-off 1-on
    switch(status){
        case 0: //off
            *(GPIOL_PIN10.GPIOx_DAT_REG) &= ~(0x1<<PIN_NUM(pin));
            break;
        case 1: //on
            *(GPIOL_PIN10.GPIOx_DAT_REG) |= (0x1<<PIN_NUM(pin));
            break;
        default:
            break;
    }
    return 0;
}


struct GPIOx_PIN *get_GPIOL_PIN10(void){
    return &GPIOL_PIN10;
}

EXPORT_SYMBOL(get_GPIOL_PIN10);
MODULE_LICENSE("GPL");

【led_driver.c】

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/err.h>
#include <asm/io.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/string.h>
#include "orangepi_plus_leds.h"


static int major = 0;
static int error_check = 0;
static int i = 0;
static struct device *device_check;
static unsigned int minor;
static int kernel_buf = 0;

static volatile int *GPIOx_base_address;
static volatile int *PA_CFGx_REG;
static volatile int *PA_DATA_REG;

static struct GPIOx_PIN *led = NULL;

static int gpio_open(struct inode *inode, struct file *file){
    printk("========== %s  %s  %d ===============\n",__FILE__,__FUNCTION__ ,__LINE__);
    dev_t dev = inode->i_rdev;  //应用层open文件的设备号
    minor = MINOR(dev);  //获取次设备号    0-1 
    switch (minor){
        case 0:
            led = get_GPIOA_PIN15();
            break;
        case 1:
            led = get_GPIOL_PIN10();
        default:
            break;
    }
    led->gpio_init(led->pin);
    return 0;
}

static ssize_t gpio_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
    printk("========== %s  %s  %d =============\n",__FILE__,__FUNCTION__ ,__LINE__);
    return 0;
}

static ssize_t gpio_write (struct file *file, const char __user *buf, size_t size, loff_t * offset){
    printk("========== %s  %s  %d  =============\n",__FILE__,__FUNCTION__ ,__LINE__);
    if(copy_from_user(&kernel_buf,buf,4)){
        pr_err("copy_from_user error.(%s)\n",__FUNCTION__);
        return -1;
    }
    led->gpio_control(led->pin,kernel_buf);
    return 0;
}
static int gpio_close(struct inode *inode, struct file *file){
    printk("========== %s  %s  %d ===============\n",__FILE__,__FUNCTION__ ,__LINE__);
    iounmap(PA_DATA_REG);
    iounmap(PA_CFGx_REG);
    iounmap(GPIOx_base_address);
    return 0;
}


static struct file_operations gpio_ops = {
    .owner = THIS_MODULE,
    .open = gpio_open,
    .release = gpio_close,
    .write = gpio_write,
    .read = gpio_read,
};

static struct class *gpio_cls;
 
static int __init gpio_driver_init(void){
    printk("============= %s  %s  %d ==================\n",__FILE__,__FUNCTION__ ,__LINE__);
    /* 1.注册字符设备 */
    major = register_chrdev(major,"GPIO",&gpio_ops);

    /* 2.创建设备节点 */
    gpio_cls = class_create(THIS_MODULE,"gpio_class");
    if(IS_ERR(gpio_cls)){
        error_check = PTR_ERR(gpio_cls);
        pr_err("class create failed.(error code:%d)\n",error_check);
        unregister_chrdev(major,"GPIO");
        return -1;
    }

    for(i=0;i<2;i++){
        device_check = device_create(gpio_cls,NULL,MKDEV(major,i),NULL,"led%d",i+1);
        if(IS_ERR(device_check)){
            error_check = PTR_ERR(device_check);
            pr_err("device create failed.(error code:%d)\n",error_check);
            class_destroy(gpio_cls);
            unregister_chrdev(major,"GPIO");
            return -1;
        }
    }
    return 0;
}

static void __exit gpio_driver_exit(void){
    printk("============= %s  %s  %d ==================\n",__FILE__,__FUNCTION__ ,__LINE__);
    iounmap(PA_DATA_REG);
    iounmap(PA_CFGx_REG);
    iounmap(GPIOx_base_address);
    device_destroy(gpio_cls, MKDEV(major,0));
    device_destroy(gpio_cls, MKDEV(major,1));
    class_destroy(gpio_cls);
    unregister_chrdev(major,"GPIO");
}

module_init(gpio_driver_init);
module_exit(gpio_driver_exit);
MODULE_LICENSE("GPL");

【Makefile】

# KERNEL_DIR = /usr/src/linux-headers-5.4.65-sunxi/
KERNEL_DIR =/home/socket/Desktop/linux-5.4-orangepi

# CROSS_COMPILE = arm-linux-gnueabihf-
CROSS_COMPILE = arm-none-linux-gnueabihf-


all:
	make -C $(KERNEL_DIR) M=`pwd` modules
	$(CROSS_COMPILE)gcc test_led_app.c -o test_led_app
clean:
	make -C $(KERNEL_DIR) M=`pwd` modules clean
	rm -rf modules.order test_led_app

OrangePiPlus_leds-y	:= led_driver.o orangepi_plus_led1.o orangepi_plus_led2.o
obj-m	+= OrangePiPlus_leds.o

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AF_INET6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值