目录
前言
一、DHT11模块
1.通信协议
2.数据格式
3.编程思路
①入口函数
②实现read函数
③编写中断处理函数
④***编写数据解析函数
⑤应用程序
二、DS18B20模块
1. 通信时序
① 初始化时序
② 写时序
③ 读时序
2. 常用命令
3. 编程思路
1.启动温度转换
2.读取温度
三、源码
1.dht11源码
2.ds18b20
课程链接
前言
在这里主要记录学习韦东山老师Linux驱动课程的笔记,韦东山老师的驱动课程讲的非常好,想要学习驱动的小伙伴可以去b站学习他的课程。
一、DHT11模块
1.通信协议
与IRDA的通信不同,需要先发一个开始信号给DHT11,才能接收数据。下图为一次完整传输示例:
其中深黑色信号表示由主机驱动,即主机向 DHT11 发信号,浅灰色信号表示 DHT11 驱动,即DHT11发向主机发信号。
⚫ 当主机没有与DHT11通信时,总线处于空闲状态,此时总线电平由于上拉 电阻的作用处于高电平。
⚫ 当主机与DHT11正在通信时,总线处于通信状态,一次完整的通信过程如 下: 开始信号 = 一个低脉冲 + 一个高脉冲。低脉冲至少持续18ms,高脉冲持续20-40us。
a) 主机将对应的GPIO管脚配置为输出,准备向DHT11发送数据;
b) 主机发送一个开始信号。
c) 主机将对应的GPIO管脚配置为输入,准备接受DHT11传来的数据,这 时信号由上拉电阻拉高; d) DHT11发出响应信号: 响应信号 = 一个低脉冲 + 一个高脉冲。低脉冲持续80us,高脉冲持续80us。
e) DHT11发出数据信号: ◼ 数据为0的一位信号 = 一个低脉冲 + 一个高脉冲。低脉冲持续 50us,高脉冲持续26~28us。 ◼ 数据为1的一位信号 = 一个低脉冲 + 一个高脉冲。低脉冲持续 50us,高脉冲持续70us。
f) DHT11发出结束信号: 最后1bit数据传送完毕后,DHT11拉低总线50us,然后释放总线,总线由 上拉电阻拉高进入空闲状态。
2.数据格式
数据格式 = 8bit 湿度整数数据+8bit湿度小数数据 +8bi 温度整数数据+8bit温度小数数据 +8bit 校验和。
数据传送正确时,校验和等于“8bit湿度整数数据+8bit湿度小数数据+8bi 温度整数数据+8bit温度小数数据”所得结果的末8位。
3.编程思路
①入口函数
- 叫gpio引脚编号转换为中断号
- 申请GPIO
- 设置DHT11的GPIO引脚的初始状态输出高电平
- 释放这个引脚
- 设置DHT11的GPIO引脚为输入模式
②实现read函数
- 发送18ms的低脉冲
- 注册中断
- 设置定时器
- 休眠等待数据(等待温湿度的整数位)
- 释放中断
- 判断数据是否有效
- 拷贝回用户空间
③编写中断处理函数
- 记录中断发生的实践
- 累加次数84次
- 次数足够解析数据放入环形缓冲区
- 唤醒read
- u64 ktime_get_ns
④***编写数据解析函数
- 计算高低脉冲持续时间
- 如果接收到约等于50微秒低电平,就判断数据是1还是0
- 当接收到八个数据说明接收完一位,将计数值都恢复到初始状态
- 将接收到的数据与校验码做对比
- 如果相等将整数部分放入环形缓冲区
- 唤醒read
⑤应用程序
- 打开设备节点
- 读取数据放入buf
- gcc 和 arm-gcc的区别
二、DS18B20模块
1. 通信时序
① 初始化时序
类似前面的DHT11,主机要跟DS18B20通信,首先需要发出一个开始信号。 深黑色线表示由主机驱动信号,浅灰色线表示由DS18B20驱动信号。 最开始时引脚是高电平,想要开始传输信号。
a) 必须要拉低至少480us,这是复位信号;
b) 然后拉高释放总线,等待15~60us之后,
c) 如果GPIO上连有DS18B20芯片,它会拉低60~240us。 如果主机在最后检查到60~240us的低脉冲,则表示DS18B20初始化成功。
② 写时序
⚫ 如果写0,拉低至少60us(写周期为60-120us)即可;
⚫ 如果写1,先拉低至少1us,然后拉高,整个写周期至少为60us即可。
如何读数据
③ 读时序
⚫ 主机先拉低至少1us,随后读取电平,如果为0,即读到的数据是0,如果 为1,即可读到的数据是1。
⚫ 整个过程必须在15us内完成,15us后引脚都会被拉高。
2. 常用命令
CCH | Skip ROM | 忽略ROM表示后续发出的命令将会给发给所有设备程序 如果总线上只有ds18b20这一个设备,则适合用这个命令 |
55H | MatchRMO | 匹配RMO发出此命令后,接着发出64位ROM编码,用于选中某个设备 |
44H | Convert Temperation | 启动温度转化,注意不同精度需要不同转化时间 结果存入内部RAM |
BEH | read | 读取整个内部RAM,9字节 |
3. 编程思路
1.启动温度转换
- 关中断
在 Linux 内核中,
spin_lock_irqsave
函数用于获取自旋锁并保存中断状态。函数原型:
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
参数:
lock
:指向要获取的自旋锁的指针。flags
:用于保存中断状态的变量。返回值:该函数没有返回值。
例如,在多线程环境中,当需要保护一段关键代码不被并发访问时,可以使用
spin_lock_irqsave
来获取自旋锁并保存中断状态。这样可以确保在执行关键代码段时,不会被其他线程或中断干扰,保证数据的一致性和正确性。
- 主控芯片发出480us的低脉冲
- 等待回应
在 Linux 内核中,
set_current_state
函数用于设置当前进程的状态。一、函数原型
void set_current_state(state_t state)
其中
state_t
是一个枚举类型,代表进程的状态。二、参数说明
state
:进程的新状态,可以是以下几种值之一:
TASK_RUNNING
:表示进程正在运行或可运行状态。TASK_INTERRUPTIBLE
:可中断的睡眠状态,等待某个条件满足时被唤醒,唤醒后可以被信号中断。TASK_UNINTERRUPTIBLE
:不可中断的睡眠状态,等待某个条件满足时被唤醒,唤醒后不能被信号中断。- 其他特定于内核的进程状态值。
三、返回值
这个函数没有返回值。
使用这个函数可以改变当前进程的状态,从而影响内核的调度行为。例如,当一个进程需要等待某个资源时,可以将其状态设置为可中断或不可中断的睡眠状态,以便在资源可用时被唤醒并继续执行。
在 Linux 内核中,
schedule_timeout
是一个用于实现任务睡眠一段时间的函数。一、函数原型
long schedule_timeout(long timeout)
二、参数说明
timeout
:指定任务睡眠的时间间隔,以 jiffies(内核时钟滴答数)为单位。通常通过HZ
(内核时钟频率)将时间转换为 jiffies。例如,如果要睡眠 2 秒,在HZ
为 100 的情况下,timeout
的值为 2 *HZ
= 200。三、返回值
返回值表示剩余的睡眠时间,如果返回值为 0,表示睡眠时间已到,任务被唤醒。如果在睡眠过程中被提前唤醒(例如通过信号),则返回剩余的睡眠时间。
这个函数通常用于内核空间中需要让当前任务睡眠一段时间的场景。需要注意的是,在使用这个函数时,任务会放弃 CPU 使用权,进入睡眠状态,直到指定的时间过去或者被其他事件唤醒。在用户空间一般不直接使用这个函数,而是使用用户空间的睡眠函数,如
usleep
、sleep
等。
- 模块发出60-240us的应答信号
- 发送rom命令
- 接着主控芯片发出选择模块的ROM地址
- 如果只有一个模块就发出忽略这个rom地址的信号(CCh)
- 发送功能命令
- 转换温度(44h)
- 恢复中断
- 等待一会
2.读取温度
- 再重复上面的步骤,但是发出的功能命令为读取温度(BEh)
- 然后读取九个字节,前八个为数据为,最后一个为检验码
- 恢复中断
- 计算CRC验证数据
三、源码
1.dht11源码
#include "asm-generic/errno-base.h"
#include "asm-generic/gpio.h"
#include "linux/jiffies.h"
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/delay.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.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/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>struct gpio_desc{int gpio;int irq;char *name;int key;struct timer_list key_timer;
} ;static struct gpio_desc gpios[] = {{115, 0, "dht11", },
};/* 主设备号 */
static int major = 0;
static struct class *gpio_class;static u64 g_dht11_irq_time[84];
static int g_dht11_irq_cnt = 0;/* 环形缓冲区 */
#define BUF_LEN 128
static char g_keys[BUF_LEN];
static int r, w;struct fasync_struct *button_fasync;static irqreturn_t dht11_isr(int irq, void *dev_id);
static void parse_dht11_datas(void);#define NEXT_POS(x) ((x+1) % BUF_LEN)static int is_key_buf_empty(void)
{return (r == w);
}static int is_key_buf_full(void)
{return (r == NEXT_POS(w));
}static void put_key(char key)
{if (!is_key_buf_full()){g_keys[w] = key;w = NEXT_POS(w);}
}static char get_key(void)
{char key = 0;if (!is_key_buf_empty()){key = g_keys[r];r = NEXT_POS(r);}return key;
}static DECLARE_WAIT_QUEUE_HEAD(gpio_wait);// static void key_timer_expire(struct timer_list *t)
static void key_timer_expire(unsigned long data)
{// 解析数据, 放入环形buffer, 唤醒APPparse_dht11_datas();
}/* 实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t dht11_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{int err;char kern_buf[2];if (size != 2)return -EINVAL;g_dht11_irq_cnt = 0;/* 1. 发送18ms的低脉冲 */err = gpio_request(gpios[0].gpio, gpios[0].name);gpio_direction_output(gpios[0].gpio, 0);gpio_free(gpios[0].gpio);mdelay(18);gpio_direction_input(gpios[0].gpio); /* 引脚变为输入方向, 由上拉电阻拉为1 *//* 2. 注册中断 */err = request_irq(gpios[0].irq, dht11_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, gpios[0].name, &gpios[0]);mod_timer(&gpios[0].key_timer, jiffies + 10); /* 3. 休眠等待数据 */wait_event_interruptible(gpio_wait, !is_key_buf_empty());free_irq(gpios[0].irq, &gpios[0]);//printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);/* 设置DHT11 GPIO引脚的初始状态: output 1 */err = gpio_request(gpios[0].gpio, gpios[0].name);if (err){printk("%s %s %d, gpio_request err\n", __FILE__, __FUNCTION__, __LINE__);}gpio_direction_output(gpios[0].gpio, 1);gpio_free(gpios[0].gpio);/* 4. copy_to_user */kern_buf[0] = get_key();kern_buf[1] = get_key();printk("get val : 0x%x, 0x%x\n", kern_buf[0], kern_buf[1]);if ((kern_buf[0] == (char)-1) && (kern_buf[1] == (char)-1)){printk("get err val\n");return -EIO;}err = copy_to_user(buf, kern_buf, 2);return 2;
}static int dht11_release (struct inode *inode, struct file *filp)
{return 0;
}/* 定义自己的file_operations结构体 */
static struct file_operations dht11_drv = {.owner = THIS_MODULE,.read = dht11_read,.release = dht11_release,
};static void parse_dht11_datas(void)
{int i;u64 high_time;unsigned char data = 0;int bits = 0;unsigned char datas[5];int byte = 0;unsigned char crc;/* 数据个数: 可能是81、82、83、84 */if (g_dht11_irq_cnt < 81){/* 出错 */put_key(-1);put_key(-1);// 唤醒APPwake_up_interruptible(&gpio_wait);g_dht11_irq_cnt = 0;return;}// 解析数据for (i = g_dht11_irq_cnt - 80; i < g_dht11_irq_cnt; i+=2){high_time = g_dht11_irq_time[i] - g_dht11_irq_time[i-1];data <<= 1;if (high_time > 50000) /* data 1 */{data |= 1;}bits++;if (bits == 8){datas[byte] = data;data = 0;bits = 0;byte++;}}// 放入环形buffercrc = datas[0] + datas[1] + datas[2] + datas[3];if (crc == datas[4]){put_key(datas[0]);put_key(datas[2]);}else{put_key(-1);put_key(-1);}g_dht11_irq_cnt = 0;// 唤醒APPwake_up_interruptible(&gpio_wait);
}static irqreturn_t dht11_isr(int irq, void *dev_id)
{struct gpio_desc *gpio_desc = dev_id;u64 time;/* 1. 记录中断发生的时间 */time = ktime_get_ns();g_dht11_irq_time[g_dht11_irq_cnt] = time;/* 2. 累计次数 */g_dht11_irq_cnt++;/* 3. 次数足够: 解析数据, 放入环形buffer, 唤醒APP */if (g_dht11_irq_cnt == 84){del_timer(&gpio_desc->key_timer);parse_dht11_datas();}return IRQ_HANDLED;
}/* 在入口函数 */
static int __init dht11_init(void)
{int err;int i;int count = sizeof(gpios)/sizeof(gpios[0]);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);for (i = 0; i < count; i++){ gpios[i].irq = gpio_to_irq(gpios[i].gpio);/* 设置DHT11 GPIO引脚的初始状态: output 1 */err = gpio_request(gpios[i].gpio, gpios[i].name);gpio_direction_output(gpios[i].gpio, 1);gpio_free(gpios[i].gpio);setup_timer(&gpios[i].key_timer, key_timer_expire, (unsigned long)&gpios[i]);//timer_setup(&gpios[i].key_timer, key_timer_expire, 0);//gpios[i].key_timer.expires = ~0;//add_timer(&gpios[i].key_timer);//err = request_irq(gpios[i].irq, dht11_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpios[i]);}/* 注册file_operations */major = register_chrdev(0, "100ask_dht11", &dht11_drv); /* /dev/gpio_desc */gpio_class = class_create(THIS_MODULE, "100ask_dht11_class");if (IS_ERR(gpio_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "100ask_dht11");return PTR_ERR(gpio_class);}device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "mydht11"); /* /dev/mydht11 */return err;
}/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数*/
static void __exit dht11_exit(void)
{int i;int count = sizeof(gpios)/sizeof(gpios[0]);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);device_destroy(gpio_class, MKDEV(major, 0));class_destroy(gpio_class);unregister_chrdev(major, "100ask_dht11");for (i = 0; i < count; i++){//free_irq(gpios[i].irq, &gpios[i]);//del_timer(&gpios[i].key_timer);}
}/* 7. 其他完善:提供设备信息,自动创建设备节点 */module_init(dht11_init);
module_exit(dht11_exit);MODULE_LICENSE("GPL");
2.ds18b20
#include "acpi/acoutput.h"
#include "asm-generic/errno-base.h"
#include "asm-generic/gpio.h"
#include "asm/gpio.h"
#include "asm/uaccess.h"
#include <linux/module.h>
#include <linux/poll.h>#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.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/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>struct gpio_desc{int gpio;int irq;char *name;int key;struct timer_list key_timer;
} ;static struct gpio_desc gpios[] = {{115, 0, "ds18b20", },
};/* 主设备号 */
static int major = 0;
static struct class *gpio_class;static spinlock_t ds18b20_spinlock;static void ds18b20_udelay(int us)
{u64 time = ktime_get_ns();while (ktime_get_ns() - time < us*1000);
}static int ds18b20_reset_and_wait_ack(void)
{int timeout = 100;gpio_set_value(gpios[0].gpio, 0);ds18b20_udelay(480);gpio_direction_input(gpios[0].gpio);/* 等待ACK */while (gpio_get_value(gpios[0].gpio) && timeout--){ds18b20_udelay(1);}if (timeout == 0)return -EIO;/* 等待ACK结束 */timeout = 300;while (!gpio_get_value(gpios[0].gpio) && timeout--){ds18b20_udelay(1);}if (timeout == 0)return -EIO;return 0;}static void ds18b20_send_cmd(unsigned char cmd)
{int i;gpio_direction_output(gpios[0].gpio, 1);for (i = 0; i < 8; i++){if (cmd & (1<<i)) {/* 发送1 */gpio_direction_output(gpios[0].gpio, 0);ds18b20_udelay(2);gpio_direction_output(gpios[0].gpio, 1);ds18b20_udelay(60);}else{/* 发送0 */gpio_direction_output(gpios[0].gpio, 0);ds18b20_udelay(60); gpio_direction_output(gpios[0].gpio, 1);}}
}static void ds18b20_read_data(unsigned char *buf)
{int i;unsigned char data = 0;gpio_direction_output(gpios[0].gpio, 1);for (i = 0; i < 8; i++){gpio_direction_output(gpios[0].gpio, 0);ds18b20_udelay(2);gpio_direction_input(gpios[0].gpio);ds18b20_udelay(15);if (gpio_get_value(gpios[0].gpio)){data |= (1<<i);}ds18b20_udelay(50);gpio_direction_output(gpios[0].gpio, 1);}buf[0] = data;
}/********************************************************/
/*DS18B20的CRC8校验程序*/
/********************************************************/
/* 参考: https://www.cnblogs.com/yuanguanghui/p/12737740.html */
static unsigned char calcrc_1byte(unsigned char abyte)
{ unsigned char i,crc_1byte; crc_1byte=0; //设定crc_1byte初值为0 for(i = 0; i < 8; i++) { if(((crc_1byte^abyte)&0x01)) { crc_1byte^=0x18; crc_1byte>>=1; crc_1byte|=0x80; } else crc_1byte>>=1; abyte>>=1; } return crc_1byte;
}/* 参考: https://www.cnblogs.com/yuanguanghui/p/12737740.html */
static unsigned char calcrc_bytes(unsigned char *p,unsigned char len)
{ unsigned char crc=0; while(len--) //len为总共要校验的字节数 { crc=calcrc_1byte(crc^*p++); } return crc; //若最终返回的crc为0,则数据传输正确
} static int ds18b20_verify_crc(unsigned char *buf)
{unsigned char crc;crc = calcrc_bytes(buf, 8);if (crc == buf[8])return 0;elsereturn -1;
}static void ds18b20_calc_val(unsigned char ds18b20_buf[], int result[])
{unsigned char tempL=0,tempH=0;unsigned int integer;unsigned char decimal1,decimal2,decimal;tempL = ds18b20_buf[0]; //读温度低8位tempH = ds18b20_buf[1]; //读温度高8位if (tempH > 0x7f) //最高位为1时温度是负{tempL = ~tempL; //补码转换,取反加一tempH = ~tempH+1; integer = tempL/16+tempH*16; //整数部分decimal1 = (tempL&0x0f)*10/16; //小数第一位decimal2 = (tempL&0x0f)*100/16%10; //小数第二位decimal = decimal1*10+decimal2; //小数两位}else{integer = tempL/16+tempH*16; //整数部分decimal1 = (tempL&0x0f)*10/16; //小数第一位decimal2 = (tempL&0x0f)*100/16%10; //小数第二位decimal = decimal1*10+decimal2; //小数两位}result[0] = integer;result[1] = decimal;
}/* 实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t ds18b20_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{unsigned long flags;int err;unsigned char kern_buf[9];int i;int result_buf[2];if (size != 8)return -EINVAL;/* 1. 启动温度转换 *//* 1.1 关中断 */spin_lock_irqsave(&ds18b20_spinlock, flags);/* 1.2 发出reset信号并等待回应 */err = ds18b20_reset_and_wait_ack();if (err){spin_unlock_irqrestore(&ds18b20_spinlock, flags);printk("ds18b20_reset_and_wait_ack err\n");return err;}/* 1.3 发出命令: skip rom, 0xcc */ds18b20_send_cmd(0xcc);/* 1.4 发出命令: 启动温度转换, 0x44 */ds18b20_send_cmd(0x44);/* 1.5 恢复中断 */spin_unlock_irqrestore(&ds18b20_spinlock, flags);/* 2. 等待温度转换成功 : 可能长达1s */set_current_state(TASK_INTERRUPTIBLE);schedule_timeout(msecs_to_jiffies(1000));/* 3. 读取温度 *//* 3.1 关中断 */spin_lock_irqsave(&ds18b20_spinlock, flags);/* 3.2 发出reset信号并等待回应 */err = ds18b20_reset_and_wait_ack();if (err){spin_unlock_irqrestore(&ds18b20_spinlock, flags);printk("ds18b20_reset_and_wait_ack second err\n");return err;}/* 3.3 发出命令: skip rom, 0xcc */ds18b20_send_cmd(0xcc);/* 3.4 发出命令: read scratchpad, 0xbe */ds18b20_send_cmd(0xbe);/* 3.5 读9字节数据 */for (i = 0; i < 9; i++){ds18b20_read_data(&kern_buf[i]);}/* 3.6 恢复中断 */spin_unlock_irqrestore(&ds18b20_spinlock, flags);/* 3.7 计算CRC验证数据 */err = ds18b20_verify_crc(kern_buf);if (err){printk("ds18b20_verify_crc err\n");return err;}/* 4. copy_to_user */ds18b20_calc_val(kern_buf, result_buf);err = copy_to_user(buf, result_buf, 8);return 8;
}/* 定义自己的file_operations结构体 */
static struct file_operations gpio_key_drv = {.owner = THIS_MODULE,.read = ds18b20_read,
};/* 在入口函数 */
static int __init ds18b20_init(void)
{int err;int i;int count = sizeof(gpios)/sizeof(gpios[0]);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);spin_lock_init(&ds18b20_spinlock);for (i = 0; i < count; i++){ err = gpio_request(gpios[i].gpio, gpios[i].name);gpio_direction_output(gpios[i].gpio, 1);}/* 注册file_operations */major = register_chrdev(0, "100ask_ds18b20", &gpio_key_drv); /* /dev/gpio_desc */gpio_class = class_create(THIS_MODULE, "100ask_ds18b20_class");if (IS_ERR(gpio_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "100ask_ds18b20");return PTR_ERR(gpio_class);}device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "myds18b20"); /* /dev/myds18b20 */return err;
}/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数*/
static void __exit ds18b20_exit(void)
{int i;int count = sizeof(gpios)/sizeof(gpios[0]);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);device_destroy(gpio_class, MKDEV(major, 0));class_destroy(gpio_class);unregister_chrdev(major, "100ask_ds18b20");for (i = 0; i < count; i++){gpio_free(gpios[i].gpio);}
}/* 7. 其他完善:提供设备信息,自动创建设备节点 */module_init(ds18b20_init);
module_exit(ds18b20_exit);MODULE_LICENSE("GPL");
课程链接
百问网韦老师的驱动入门实验班https://video.100ask.net/p/t_pc/course_pc_detail/video/v_637cc4a7e4b068e9c3dc6633?product_id=p_634cbce4e4b00a4f37500252&content_app_id=&type=6