成都科技网站建设联系电话,ui培训机构推荐,最新app推广,泰安做网站字符设备驱动
〇、基本知识
设备驱动分类 #xff08;按共性分类方便管理#xff09;
1.字符设备驱动 字符设备指那些必须按字节流传输#xff0c;以串行顺序依次进行访问的设备。它们是我们日常最常见的驱动了#xff0c;像鼠标、键盘、打印机、触摸屏#xff0c;还有…字符设备驱动
〇、基本知识
设备驱动分类 按共性分类方便管理
1.字符设备驱动 字符设备指那些必须按字节流传输以串行顺序依次进行访问的设备。它们是我们日常最常见的驱动了像鼠标、键盘、打印机、触摸屏还有点灯以及I2C、SPI、音视频都属于字符设备驱动。
字符设备不经过系统快速缓冲。
2.块设备驱动 就是存储器设备的驱动比如 EMMC、NAND、SD 卡和 U 盘等存储设备因为这些存储设备的特点是以存储块为基础按块随机访问可以用任意顺序进行访问以块为单位进行操作因此叫做块设备。数据的读写只能以块(通常是512B)的倍数 进行。与字符设备不同块设备并不支持基于字符的寻址。
块设备经过设备缓冲
3.网络设备驱动 就是网络驱动不管是有线的还是无线的都属于网络设备驱动的范畴。按TCP/IP协议栈传输。
网络设备面向数据包的接受和发送而设计它并不对应文件系统的节点
注意 块设备和网络设备驱动要比字符设备驱动复杂就是因为其复杂所以半导体厂商一般都编写好了大多数情况下都是直接可以使用的。
一个设备可以属于多种设备驱动类型比如USB WIFI其使用 USB 接口属于字符设备但是其又能上网所以也属于网络设备驱动。
设备驱动框架
为了安全 一切皆文件
为了标准化操作函数方便对接工作
open read write close
字符设备框架 字符设备驱动编写三部曲 注册设备号初始化字符设备实现需要的文件操作 一、注册设备号
为了让内核知道这个设备是合法的将构造的设备号注册到内核中表明该设备号已经被占用如果有其他驱动随后要注册该设备号将会失败。
主次设备号MKDEVregister_chrdev_region
驱动部分
00_头文件
#include linux/fs.h //for MKDEV register_chrdev_region01_主次设备号
#define LED_MA 500 //主设备号 用于区分不同种类的设备 //某些主设备号已经静态地分配给了大部分公用设备。见Documentation/devices.txt 。
#define LED_MI 0 //次设备号 用于区分同一类型的多个设备
#define LED_NUM 1 //有多少个设备02_注册字符设备号 dev_t devno MKDEV(LED_MA, LED_MI); int ret;ret register_chrdev_region(devno, LED_NUM, yhai_led); /*注册字符设备号(静态分配)为了让内核认可为一个字符驱动获取一个或多个设备编号dev_id: 分配的起始设备编号(常常是0DEVICE_NUM 请求的连续设备编号的总数(不能太大避免别的主设备号冲突)DEVICE_NAME 是应当连接到这个编号范围的设备的名字 alloc_chrdev_region 可进行动态分配 */if (ret 0) {printk(register_chrdev_region\n);return ret;}03_取消注册 dev_t devno MKDEV(LED_MA, LED_MI);unregister_chrdev_region(devno, LED_NUM); //取消注册总程序
//led.c
#include linux/kernel.h
#include linux/module.h
#include linux/fs.h //for MKDEV register_chrdev_region#define LED_MA 500 //主设备号 用于区分不同种类的设备 //某些主设备号已经静态地分配给了大部分公用设备。见Documentation/devices.txt 。
#define LED_MI 0 //次设备号 用于区分同一类型的多个设备
#define LED_NUM 1 //有多少个设备static int led_init(void)
{dev_t devno MKDEV(LED_MA, LED_MI); int ret;ret register_chrdev_region(devno, LED_NUM, yhai_led); /*注册字符设备号(静态分配)为一个字符驱动获取一个或多个设备编号dev_id: 分配的起始设备编号(常常是0DEVICE_NUM 请求的连续设备编号的总数(不能太大避免别的主设备号冲突)DEVICE_NAME 是应当连接到这个编号范围的设备的名字 alloc_chrdev_region 可进行动态分配 */if (ret 0) {printk(register_chrdev_region\n);return ret;}printk(led init\n);return 0; //返回值 0:成功 负值:失败
}static void led_exit(void)
{dev_t devno MKDEV(LED_MA, LED_MI);unregister_chrdev_region(devno, LED_NUM); //取消注册printk(led exit\n);
}module_init(led_init); //模块加载入口声明
module_exit(led_exit); //模块卸载入口声明
MODULE_LICENSE(GPL); //模块免费开源声明验证测试
# insmod led.ko /*加载模块
# rmmod led //卸载模块 二、初始化字符设备
连接设备号对应的操作
file_operationscdev_init 连接设备号对应的操作cdev_add 添加到散列表里面放着一堆字符设备。应用层open时根据设备号在散列表中找到设备open返回的fd找到对应file结构然后调用相应操作
驱动部分
00_头文件
#include linux/cdev.h //字符设备头文件01_字符设备初始化
struct file_operations led_fops 这部分全是函数指针
struct cdev cdev; //定义字符设备static int led_open(struct inode *inode, struct file *file)
{printk(driver led open\n);return 0;
}static int led_release(struct inode *inode, struct file *file)
{printk(driver led close\n);return 0;
}struct file_operations led_fops { //文件操作(一切皆文件).owner THIS_MODULE,.open led_open,.release led_release,
};cdev_init(cdev, led_fops);//字符设备初始化ret cdev_add(cdev, devno, LED_NUM); //添加字符设备到系统中if (ret 0) {printk(cdev_add\n);return ret;}02_字符设备删除
这个删完再取消注册相当于把空间中的内容都清掉再把空间释放 cdev_del(cdev)应用部分
交叉编译aarch64-linux-gnu-gcc app.c
//app.c
#include stdio.h
#include fcntl.h
#include unistd.h
#include stdlib.h
#include sys/ioctl.hint main(int argc, char **argv)
{int fd;fd open(/dev/led, O_RDWR);if (fd 0) {perror(open);exit(1);}printf(open led ok\n); //注意要加\n 否则打印信息可能没有return 0;
}总程序
//led.c
#include linux/kernel.h
#include linux/module.h
#include linux/fs.h //for MKDEV register_chrdev_region
#include linux/cdev.h //字符设备头文件#define LED_MA 500 //主设备号 用于区分不同种类的设备 //某些主设备号已经静态地分配给了大部分公用设备。见Documentation/devices.txt。
#define LED_MI 0 //次设备号 用于区分同一类型的多个设备
#define LED_NUM 1 //有多少个设备struct cdev cdev; //定义字符设备static int led_open(struct inode *inode, struct file *file)
{printk(driver led open\n);return 0;
}static int led_release(struct inode *inode, struct file *file)
{printk(driver led close\n);return 0;
}struct file_operations led_fops { //文件操作(一切皆文件).owner THIS_MODULE,.open led_open,.release led_release,
};static int led_init(void)
{dev_t devno MKDEV(LED_MA, LED_MI); int ret;ret register_chrdev_region(devno, LED_NUM, yhai_led); /*注册字符设备号(静态分配)为一个字符驱动获取一个或多个设备编号dev_id: 分配的起始设备编号(常常是0DEVICE_NUM 请求的连续设备编号的总数(不能太大避免别的主设备号冲突)DEVICE_NAME 是应当连接到这个编号范围的设备的名字 alloc_chrdev_region 可进行动态分配 */if (ret 0) { //要进行异常判断printk(register_chrdev_region\n);return ret;}cdev_init(cdev, led_fops);//字符设备初始化ret cdev_add(cdev, devno, LED_NUM); //添加字符设备到系统中if (ret 0) {printk(cdev_add\n);return ret;}printk(led init\n);return 0; //返回值 0:成功 负值:失败
}static void led_exit(void)
{dev_t devno MKDEV(LED_MA, LED_MI);cdev_del(cdev)unregister_chrdev_region(devno, LED_NUM); //取消注册printk(led exit\n);
}module_init(led_init); //模块加载入口声明
module_exit(led_exit); //模块卸载入口声明
MODULE_LICENSE(GPL); //模块免费开源声明验证测试
$ make
$ aarch64-linux-gnu-gcc app.c //编译应用程序生成a.out
$ cp led.ko a.out /nfs/rootfs$ insmod led.ko
$ mknod /dev/led c 500 0 //创建设备文件应用才能访问它. ( ls -l /dev 可以看到很多其它设备文件)
$./a.out //运行 成功可看到 open led ok $ rmmod led.ko三、实现定制文件操作
幻数加密定义命令防止不同驱动间命令错乱内核与应用层间ioremap内核与硬件间,不能直接操作硬件goto语句跳到对应err位置实现逆序释放
驱动部分
00_头文件
#include asm/io.h //io操作的头文件(for ioremap readl)01_定制ioctrl操作命令部分
#define LED_MAGIC L //幻数0~0xff的数。用于区分不同的驱动, 见Documentation/ioctl/ioctl-number.txt
#define LED_ON _IOW(LED_MAGIC, 0, int) //加幻数方式来定义命令防止不同驱动间命令错乱
#define LED_OFF _IOW(LED_MAGIC, 1, int)//ioctl 用于定制操作
static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{switch (cmd) {case LED_ON:led_on();break;case LED_OFF:led_off();break;default: //异常处理printk(no found this cmd %d,cmd);return -1;}return 0;
}struct file_operations led_fops { //文件操作(一切皆文件).unlocked_ioctl led_ioctl,
}; 02_硬件控制部分
//电路连接
地线 //接 40pin 接口的40脚 gnd
控制线 //接 40pin 接口的12脚 - 管脚转换表 - 电路图 - 芯片手册 #define GPIO3 0x6000D200 // 第3个Bank GPIO 的基地址
#define CNF 0x04 //配置寄存器 (0:GPIO 1:SFIO) 偏移量
#define OE 0x14 //输出使能寄存器 (1:使能 0:关闭)
#define OUT 0x24 //输出寄存器1高电平 0低电平
#define MSK_CNF 0x84 //配置屏蔽寄存器(高位1:屏蔽 高位0:不屏蔽 低位1:GPIO模式 低位0:SFIO模式)
#define MSK_OE 0x94 //输出使能屏蔽寄存器(高位1:禁止写 低位1:使能)
#define MSK_OUT 0xA4 //输出屏蔽寄存器(高位1:禁止写 低位1:高电平)
#define PINMUX_AUX_DAP4_SCLK_0 0x70003150 //管脚复用设置unsigned char *gpio_base;
unsigned char *gpio_pinmux;//开灯
void led_on(void)
{writel(readl(gpio_baseOUT) | 1 7, gpio_baseOUT); //引脚输出高电平点亮灯printk(out put high ,led on 输出高电平点亮灯\n);
}//关灯
void led_off(void)
{writel(readl(gpio_baseOUT) ~(1 7), gpio_baseOUT); //引脚输出低电平灭灯printk(out put low, led off 输出低电平灭灯\n);
}static int led_init(void)
{//硬件初始化(成功可看到灯亮)//a.管脚复用的设置设置做GPIO功能gpio_pinmux ioremap(PINMUX_AUX_DAP4_SCLK_0, 8); /*从物理地址PINMUX_AUX_DAP4_SCLK_0开始映射8字节长度的空间到内核空间动态映射 物理地址 到内核虚拟地址phys_addr 起始物理地址size 映射范围大小单位字节返回值 映射后的内核虚拟地址 */ if (gpio_pinmux NULL) {printk(ioremap gpio_pinmux error\n);goto err3;}writel((readl(gpio_pinmux) ~(1 4))|1, gpio_pinmux); /*管脚复用配置用于 GPIO1:0 I2S4B PM: 0 I2S4B 1 RSVD1 2 RSVD2 3 RSVD3 设为非0表示不用作I2S功能则默认用做GPIO功能4 TRISTATE TRISTATE: 0 PASSTHROUGH 1 TRISTATE设为0设为直通状态才能驱动外面的设备见 9.5.1 Per Pad OptionsTristate 高阻态 - 与外界是断开的默认启动设为高阻太避免驱动影响外面的设备 passthrough 直通态 - 才能驱动外面设备 *///b. 做GPIO功能时的内部配置gpio_base ioremap(GPIO3, 0xFF); if (gpio_base NULL) {printk(ioremap gpio_base error\n);goto err2;}writel(readl(gpio_baseCNF) | 1 7, gpio_baseCNF); //配置引脚GPIO3_PJ.07 为 GPIO模式writel(readl(gpio_baseOE) | 1 7, gpio_baseOE); //使能引脚(7号)writel(readl(gpio_baseOUT) | 1 7, gpio_baseOUT); //输出高电平点亮灯writel(readl(gpio_baseMSK_CNF) | 1 7, gpio_baseMSK_CNF); //取消对GPIO模下引脚的屏蔽writel(readl(gpio_baseMSK_OE) | 1 7, gpio_baseMSK_OE); //取消引脚 使能屏蔽}03_顺序申请逆序释放
static int led_init(void)
{ret cdev_add(cdev, devno, LED_NUM);if (ret 0) {printk(cdev_add\n);goto err1;}gpio_base ioremap(GPIO3, 0xFF); if (gpio_base NULL) {printk(ioremap gpio_base error\n);goto err2;}gpio_pinmux ioremap(PINMUX_AUX_DAP4_SCLK_0, 8);if (gpio_pinmux NULL) {printk(ioremap gpio_pinmux error\n);goto err3;}err3: //跳过来后就顺序执行下面的顺序释放iounmap(gpio_base);
err2:cdev_del(cdev);
err1: //报错就释放上一步做完的unregister_chrdev_region(devno, LED_NUM); return ret;
}应用部分
#include stdio.h
#include fcntl.h
#include unistd.h
#include stdlib.h
#include sys/ioctl.h#define LED_MAGIC L //幻数,一般一个驱动一个幻数和驱动部分幻数一致//用幻数加密后避免程序误操作有写错的时候有安全问题#define LED_ON _IOW(LED_MAGIC, 0, int) //用幻数加密控制命令
#define LED_OFF _IOW(LED_MAGIC, 1, int)int main(int argc, char **argv)
{int fd;fd open(/dev/led, O_RDWR); //打开设备文件if (fd 0) {perror(open);exit(1);}while(1){ioctl(fd, LED_ON); //发送控制命令 LED_ONusleep(100000);ioctl(fd, LED_OFF); //发送控制命令 LED_OFFusleep(100000);}return 0;
}struct file_operations led_fops { //文件操作(一切皆文件).owner THIS_MODULE,.open led_open,.release led_release,.unlocked_ioctl led_ioctl,
};总程序
#include linux/kernel.h
#include linux/module.h //模块的头文件 (for module_init MODULE_LICENSE)
#include linux/fs.h //for MKDEV register_chrdev_region
#include linux/cdev.h //字符设备头文件#include asm/io.h //io操作的头文件(for ioremap readl)#define LED_MA 500 //主设备号 用于区分不同种类的设备 //某些主设备号已经静态地分配给了大部分公用设备。见Documentation/devices.txt 。
#define LED_MI 0 //次设备号 用于区分同一类型的多个设备
#define LED_NUM 1 //有多少个设备struct cdev cdev; //定义字符设备#define LED_MAGIC L //幻数0~0xff的数。用于区分不同的驱动, 见Documentation/ioctl/ioctl-number.txt
#define LED_ON _IOW(LED_MAGIC, 0, int) //加幻数方式来定义命令防止不同驱动间命令错乱
#define LED_OFF _IOW(LED_MAGIC, 1, int)#define GPIO3 0x6000D200 //第3个Bank GPIO 的基地址 (GPIO3_PJ.07)
#define CNF 0x04 //配置寄存器 (0:GPIO 1:SFIO) 偏移量
#define OE 0x14 //输出使能寄存器 (1:使能 0:关闭)
#define OUT 0x24 //输出寄存器1高电平 0低电平
#define MSK_CNF 0x84 //配置屏蔽寄存器(高位1:屏蔽 高位0:不屏蔽 低位1:GPIO模式 低位0:SFIO模式)
#define MSK_OE 0x94 //输出使能屏蔽寄存器(高位1:禁止写 低位1:使能)
#define MSK_OUT 0xA4 //输出屏蔽寄存器(高位1:禁止写 低位1:高电平)
#define PINMUX_AUX_DAP4_SCLK_0 0x70003150 //管脚复用设置unsigned char *gpio_base;
unsigned char *gpio_pinmux;//查看相关寄存器的内容-方便查BUG
void show_reg(void)
{printk( cnf %x\n,readl(gpio_baseCNF)); //通过基地址加偏移量来访问对应的配置寄存器printk( oe %x\n,readl(gpio_baseOE));printk( out %x\n,readl(gpio_baseOUT));printk(mask cnf %x\n,readl(gpio_baseMSK_CNF));printk(mask oe %x\n,readl(gpio_baseMSK_OE));printk(mask out %x\n,readl(gpio_baseMSK_OUT));printk(gpio_pinmux %x\n,readl(gpio_pinmux));
}static int led_open(struct inode *inode, struct file *file)
{printk(driver led open ok\n);show_reg();return 0;
}static int led_release(struct inode *inode, struct file *file)
{printk(driver led close ok\n);show_reg();return 0;
}//开灯
void led_on(void)
{writel(readl(gpio_baseOUT) | 1 7, gpio_baseOUT); //引脚输出高电平点亮灯printk(out put high ,led on 输出高电平点亮灯\n);
}//关灯
void led_off(void)
{writel(readl(gpio_baseOUT) ~(1 7), gpio_baseOUT); //引脚输出低电平灭灯printk(out put low, led off 输出低电平灭灯\n);
}//ioctl 用于定制操作
static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{switch (cmd) {case LED_ON:led_on();break;case LED_OFF:led_off();break;default: //异常处理printk(no found this cmd %d,cmd);return -1;}return 0;
}//3.实现需要的文件操作
// file_operations 中 定义了针对文件的一系列操作方法 不是每个都需实现
struct file_operations led_fops { //文件操作(一切皆文件).owner THIS_MODULE,.open led_open,.release led_release,.unlocked_ioctl led_ioctl,
};static int led_init(void)
{dev_t devno MKDEV(LED_MA, LED_MI); int ret;//1.注册设备号ret register_chrdev_region(devno, LED_NUM, yhai_led); /*注册字符设备号(静态分配)为一个字符驱动获取一个或多个设备编号dev_id: 分配的起始设备编号(常常是0DEVICE_NUM 请求的连续设备编号的总数(不能太大避免别的主设备号冲突)DEVICE_NAME 是应当连接到这个编号范围的设备的名字 alloc_chrdev_region 可进行动态分配 */if (ret 0) { //要进行异常判断printk(register_chrdev_region\n);return ret;}//2.初始化字符设备cdev_init(cdev, led_fops);//字符设备初始化ret cdev_add(cdev, devno, LED_NUM); //添加字符设备到系统中if (ret 0) {printk(cdev_add\n);goto err1;}//硬件初始化(成功可看到灯亮)//a.管脚复用的设置设置做GPIO功能gpio_pinmux ioremap(PINMUX_AUX_DAP4_SCLK_0, 8); /*从物理地址PINMUX_AUX_DAP4_SCLK_0开始映射 8字节长度的空间到内核空间动态映射 物理地址 到内核虚拟地址phys_addr 起始物理地址size 映射范围大小单位字节返回值 映射后的内核虚拟地址 */ if (gpio_pinmux NULL) {printk(ioremap gpio_pinmux error\n);goto err3;}writel((readl(gpio_pinmux) ~(1 4))|1, gpio_pinmux); /*管脚复用配置用于 GPIO1:0 I2S4B PM: 0 I2S4B 1 RSVD1 2 RSVD2 3 RSVD3 设为非0表示不用作I2S功能则默认用做GPIO功能4 TRISTATE TRISTATE: 0 PASSTHROUGH 1 TRISTATE设为0设为直通状态才能驱动外面的设备见 9.5.1 Per Pad OptionsTristate 高阻态 - 与外界是断开的默认启动设为高阻太避免驱动影响外面的设备 passthrough 直通态 - 才能驱动外面设备 *///b. 做GPIO功能时的内部配置gpio_base ioremap(GPIO3, 0xFF); if (gpio_base NULL) {printk(ioremap gpio_base error\n);goto err2;}writel(readl(gpio_baseCNF) | 1 7, gpio_baseCNF); //配置引脚GPIO3_PJ.07 为 GPIO模式writel(readl(gpio_baseOE) | 1 7, gpio_baseOE); //使能引脚(7号)writel(readl(gpio_baseOUT) | 1 7, gpio_baseOUT); //输出高电平点亮灯writel(readl(gpio_baseMSK_CNF) | 1 7, gpio_baseMSK_CNF); //取消对GPIO模下引脚的屏蔽writel(readl(gpio_baseMSK_OE) | 1 7, gpio_baseMSK_OE); //取消引脚 使能屏蔽printk(led init ok\n);return 0; //返回值 0:成功 负值:失败//goto 出错处理 顺序申请逆序释放避免资源回收不完全如内存泄露
err3:iounmap(gpio_base);
err2:cdev_del(cdev);
err1:unregister_chrdev_region(devno, LED_NUM);return ret;
}static void led_exit(void)
{//要配对释放资源,逆序释放资源dev_t devno MKDEV(LED_MA, LED_MI);iounmap(gpio_base); //取消映射 iounmap(gpio_pinmux); cdev_del(cdev); //从系统中移除该设备unregister_chrdev_region(devno, LED_NUM); //取消注册printk(led exit ok\n);
}module_init(led_init); //模块加载入口声明
module_exit(led_exit); //模块卸载入口声明
MODULE_LICENSE(GPL); //模块免费开源声明验证测试
$ make
$ aarch64-linux-gnu-gcc app.c //编译应用程序生成a.out
$ cp led.ko a.out /nfs/rootfs
# insmod led.ko
# mknod /dev/led c 500 0 //创建设备文件
#./a.out //运行 成功可看到 灯闪烁四、实现读写文件操作
应用空间的buf不能直接拷贝到内核空间采用copy_from_user错误码数据长度等问题
驱动部分
//led.c
#include asm/uaccess.h //for read write#define C_BUF_LEN 64
char c_buf[C_BUF_LEN];//返回值 正数成功写入的字节数 负值错误码 0:无数据成功写入
static ssize_t led_write (struct file *file, const char __user *buf, //file: 文件指针 buf:用户空间的缓冲区size_t count, loff_t * f_pos) //count: 数据长度 f_pos: 文件位置
{ssize_t ret 0; printk (Writing %ld bytes\n, count); if (count C_BUF_LEN -1) return -ENOMEM; if (count0) return -EINVAL; /*应用空间的buf不能直接拷贝到内核空间while(count--){*c_buf buf}*/ if (copy_from_user (c_buf, buf, count)) { /*从用户空间拷贝数据到内核空间unsigned long copy_from_user(void * to, const void __user * from, unsigned long n) to内核空间的目标缓冲区from: 应用空间源缓冲区n: 拷贝的长度 返回值 0 成功 正数没有拷贝成功的字节数*/ret -EFAULT; } else { c_buf[63]\0; printk (Received: %s\n, c_buf); ret count; } return ret;
}static ssize_t led_read(struct file *file, char *buff, size_t count, loff_t *offp)
{ssize_t result 0; if(count C_BUF_LEN -1 ) count C_BUF_LEN -1; if(count 0) return -EINVAL; if (copy_to_user(buff,c_buf, count)) result -EFAULT; else printk (read %ld bytes\n, count); result count; return result;
}struct file_operations led_fops {.write led_write,.read led_read,
};应用部分
//app.c
#include stdio.h
#include fcntl.h
#include unistd.h
#include stdlib.h
#include sys/ioctl.h
#include string.hint main(int argc, char **argv)
{int fd;char buff[] lets go ;fd open(/dev/led, O_RDWR);if (fd 0) {perror(open);exit(1);}write (fd, buff, sizeof(buff));memset(buff,\0,sizeof(buff));read (fd, buff, sizeof(buff) - 1);printf(read buf is %s\n,buff);return 0;
}验证测试
$ make
$ aarch64-linux-gnu-gcc app.c
$ cp a.out led.ko /nfs/rootfs
# setenv bootargs root/dev/nfs rw nfsroot192.168.9.119:/nfs/rootfs,v3 consolettyS0,115200 init/linuxrc ip192.168.9.9
# setenv nfsboot ext4load mmc 1:1 0x84000000 /boot/Image \; ext4load mmc 1:1 83100000 /boot/tegra210-p3448-0002-p3449-0000-b00.dtb \; booti 0x84000000 - 83100000
# run nfsboot //成功 可看到 read buf is lets go 即读出的数据和写入的一致