想必看過我很多次博客的同學,都知道了編寫驅動的流程!
這里我們還是按照以前的習慣來一步一步講解!
單總線驅動,在F103和51單片機的裸機開發中是經常見的。
linux驅動代碼編寫實際上就是,端對端的編程!
就是 硬件-連接-軟件
一開始是主芯片的設備樹和鏡像,配置硬件,該執行哪條總線,端口。
二就是編寫外設寫入或者讀取數據的文件,還有類似QT的代碼執行。
三就是需要把需要驅動的硬件目標和軟件操作文件進行匹配。也就是將第一步和第二步相匹配。
1、配置設備樹
打開 stm32mp157d-atk.dts 文件,把以下的內容添加到此文件中
與之前大多數不同的是,這里并不是節點追加的方式。是新創建的。
這里就不用配置鏡像了,因為沒有用到追加節點。就用到一個GPIO口。
一般來說下面我們這個代碼用到了platform框架,那么就需要用到pinctrl用來配置電氣屬性的,但是這里正點原子并沒有加pinctrl,可能是因為該引腳復位后默認就是 GPIO 功能,就無需 pinctrl 配置 “復用為 GPIO”,但是這樣并不規范!
編譯:
make dtbs
復制到開發板中:
sudo cp arch/arm/boot/dts/stm32mp157d-atk.dtb /home/chensir/linux/tftpboot/ -f
發現可以在設備樹下發現我們剛剛創建的節點;
2、DS18B20驅動編寫
之前的博客也是跟大家按照肌肉記憶來編寫程序!一步一步按照思路來編寫!
總代碼會放在最后。
為了讓大家更能明白,可以先對著總代碼,進行對我的寫代碼流程更加詳細得當!
2.1、頭文件
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/errno.h>
可以看出頭文件用到了miscdevice.h,說明這個例程代碼用到了MISC子系統。
2.2、驅動注冊和注銷
因為我這個DS18B20外設的單總線是在linux沒有總線概念的,所以用platform總線來實現總線驅動框架,這個是前面已經講過的!這個僅僅為了簡化代碼量!
注冊和注銷一體化:這個意思是init和exit不用開發人員寫了!
可以看以下舉例代碼:
2.2.1、編寫platform_driver驅動結構體
其中流程是:
16行代碼:設備樹中的compatible值會與ds18b20_of_match下的compatible相匹配。
如下圖ds18b20_of_match的代碼:
其中MODULE_DEVICE_TABLE是聲明一下而已!
15行代碼:會在driver目錄下生成ds18b20。這個是驅動開發者自己編寫的!
和設備樹中的compatible沒有關系。
18~19行代碼:compatible值一旦匹配成功,就會執行probe和remove。(這些其實之前的驅動程序講解已經講過很多遍了)
2.2.2、編寫probe和remove函數
2.2.3、注冊和注銷字符驅動設備
我們這里用到的MISC子系統和platform框架,所以可以回顧。
我們用這個的同時需要在設備結構體中定義MISC設備。
雖然說MISC子系統幫開發人員自動設置了主設備號為10,但是子設備號、設備名、字符操作集還是得開發人員自己創建。
1、定義設備結構體
struct miscdevice mdev;
這里可以放在probe函數內,但是如果需要適應創建多個MISC設備,那么就放在設備結構體中;
這里我們要強調一下;
這兩種寫法要好好記一下!
2、配置probe和remove函數
這里我們先編譯測試一下;
發現并沒有問題!
接下來繼續完善probe和remove函數:
可以看到我們添加了名字、次設備號、字符操作集函數。
接下來注冊和注銷MISC設備:
==重點來了!!!==我們發現,只有在probe函數內,才動態分配了內存,在remove是沒有分配,不能傻傻的再用devm_kmallo函數了,在 probe
函數中動態分配的,remove
函數無法直接訪問,需要通過 platform_set_drvdata
和 platform_get_drvdata
傳遞指針。
接下來繼續完善:
在probe內使用 platform_set_drvdata(pdev, ds18b20_dev);
在remove內使用 ds18b20_dev = platform_get_drvdata(pdev);
3、配置字符操作集
目前這個單總線DS18B20,功能實現只需要讀數據就行,所以字符操作集只涉及,在本模塊下執行、open、release、read即可!
在完成寫完字符操作集之前,我們先來回顧DS18B20的時序,需要嚴格特定的時序,還有數據判定。然后再上傳數據。
4、獲取設備節點(設備樹屬性)
4.1、配置設備樹結構體
4.2、GPIO初始化
使用了 devm_gpio_request
(帶 devm_
前綴的資源申請函數)—— 這類函數申請的 GPIO 會與「設備生命周期」自動綁定,無需手動調用 gpio_free
,內核會在設備卸載時自動釋放 GPIO。
這里知道了有關驅動的gpio信息,僅僅是能知道信息,并沒有驅動能力,所以要向內核申請權限來驅動gpio口。
5、配置DS18B20時序
熟悉DS18B20配置的同學就知道,需要嚴格的時序,還有高低電平轉換!所以接下來我們需要配置高低電平、定時器、還有可以把這個讀寫時序放到隊列里面!
5.1、配置輸入輸出
需要配置輸入輸出!
因為需要獲取DS18B20的溫度數據,所以需要判定GPIO的值。
5.2、獲取GPIO的值
5.3、設置定時器
5.4、配置工作隊列
其中:
struct ds18b20_dev *ds18b20_dev = container_of(work, struct ds18b20_dev, work);container_of需要放在處理函數中,通過工作隊列(work)反向找到設備結構體(匹配設備)
5.5、完善定時任務
5.6、完成DS18B20的時序
5.6.1、初始化DS18B20
5.6.2、寫入一位數據
5.6.3、讀取一位數據
5.6.4、寫一個字節到DS18B20
5.6.5、從DS18B20讀取一個字節
5.7、完善字符操作集
5.8、完善工作隊列
編譯生成ko文件:
make
復制到
sudo cp ds18b20.ko /home/chensir/linux/nfs/rootfs/lib/modules/5.4.31/
6、編寫測試 APP
這里其實也簡單,就是傳遞2個數據!
核心代碼:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"int main()
{int fd, ret;unsigned char result[2];int TH, TL;short tmp = 0;float temperature;int flag = 0;fd = open("/dev/ds18b20", 0);if(fd < 0){perror("open device failed\n");exit(1);}elseprintf("Open success!\n");while(1){ret = read(fd, &result, sizeof(result)); if(ret == 0) { /* 讀取到數據 */TL = result[0];TH = result[1];if((TH == 0XFF) && (TL == 0XFF))/* 如果讀取到數據為0XFFFF就跳出本次循序 */continue;if(TH > 7) { /* 負數處理 */TH = ~TH;TL = ~TL;flag = 1; /* 標記為負數 */}tmp = TH;tmp <<= 8;tmp += TL;if(flag == 1) {temperature = (float)(tmp+1)*0.0625; /* 計算負數的溫度 */temperature = -temperature;}else {temperature = (float)tmp *0.0625; /* 計算正數的溫度 */} if(temperature < 125 && temperature > -55) { /* 溫度范圍 */printf("Current Temperature: %f\n", temperature);}}else if(ret == -1){perror("read"); break;}flag = 0;sleep(1);}close(fd); /* 關閉文件 */
}
編譯:
arm-none-linux-gnueabihf-gcc ds18b20App.c -o ds18b20App
復制到
sudo cp ds18b20App /home/chensir/linux/nfs/rootfs/lib/modules/5.4.31/
8、效果
9、總代碼:
ds18b20.c:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>
/* #include <linux/ide.h> */
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/errno.h>
#include <linux/uaccess.h>
#include <linux/timer.h>
#include <linux/workqueue.h>/*ds18b20設備結構體*/
struct ds18b20_dev{struct miscdevice mdev; /* MISC設備 */struct device_node *nd; //設備樹節點指針int ds18b20_gpio; //GPIO編號unsigned char data[2]; /* 接收原始數據的BUFF */struct timer_list timer; /* 定時器 */struct work_struct work; /* 工作隊列 */
};#define HIGH 1
#define LOW 0/** @description : 設置GPIO的輸出值* @param - value: 輸出value的值 * @return : 無*/
static void ds18b20_set_output(struct ds18b20_dev *dev, int value)
{if(value)gpio_direction_output(dev->ds18b20_gpio, 1);elsegpio_direction_output(dev->ds18b20_gpio, 0);
}/** @description : 設置GPIO為輸入模式* @param : 無* @return : 無*/
static void ds18b20_set_input(struct ds18b20_dev *dev)
{gpio_direction_input(dev->ds18b20_gpio);
}/** @description : 獲取GPIO的值* @param : 無 * @return : GPIO的電平*/
static int ds18b20_get_io(struct ds18b20_dev *dev)
{return gpio_get_value(dev->ds18b20_gpio);
}/** @description : 寫一位數據* @param bit : 要寫入的位數* @return : 無*/
static void ds18b20_write_bit(struct ds18b20_dev *dev, int bit)
{local_irq_disable();if(bit) {ds18b20_set_output(dev, LOW);udelay(5);ds18b20_set_input(dev); /* 釋放為高阻 */udelay(55); /* 補足到 ~60us */} else {ds18b20_set_output(dev, LOW);udelay(60); /* 寫0保持低 ~60us */ds18b20_set_input(dev); /* 釋放為高阻 */udelay(5);}local_irq_enable();
}/** @description : 讀一位數據* @param : 無* @return : 返回讀取一位的數據*/
static int ds18b20_read_bit(struct ds18b20_dev *dev)
{u8 bit = 0;local_irq_disable();ds18b20_set_output(dev, LOW);udelay(2);ds18b20_set_input(dev);udelay(12);bit = ds18b20_get_io(dev) ? 1 : 0;udelay(45);local_irq_enable();return bit;
}/** @description : 寫一個字節到DS18B20* @param byte : 要寫入的字節* @return : 無*/
static void ds18b20_write_byte(struct ds18b20_dev *dev, u8 byte)
{int i;for(i = 0; i < 8; i++) {if(byte & 0x01)ds18b20_write_bit(dev,1); /* write 1 */elseds18b20_write_bit(dev,0); /* write 0 */byte >>= 1; /* 右移一位獲取高一位的數據 */}
}/** @description : 讀取一個字節的數據* @param : 無* @return : 讀取到的數據*/
static char ds18b20_read_byte(struct ds18b20_dev *dev)
{int i;u8 byte = 0;for(i = 0; i < 8; i++) { /* DS18B20先輸出低位數據 ,高位數據后輸出 */if(ds18b20_read_bit(dev))byte |= (1 << i);elsebyte &= ~(1 << i);}return byte;
}/** @description : GPIO的初始化函數* @param pdev : platform設備 * @return : 0表示轉換成功,其它值表示轉換失敗*/
static int ds18b20_request_gpio(struct platform_device *pdev)
{struct device *dev = &pdev->dev;struct ds18b20_dev *ds18b20_dev = platform_get_drvdata(pdev);int ret;ds18b20_dev->nd = dev->of_node;if (!ds18b20_dev->nd)return -EINVAL;ds18b20_dev->ds18b20_gpio = of_get_named_gpio(ds18b20_dev->nd, "ds18b20-gpio", 0);if (!gpio_is_valid(ds18b20_dev->ds18b20_gpio))return -EINVAL;ret = devm_gpio_request(dev, ds18b20_dev->ds18b20_gpio, "DS18B20 Gpio");if (ret)return ret;ds18b20_set_input(ds18b20_dev);return 0;
}/** @description : 初始化DS18B20* @param : 無* @return : 0,初始化成功,1,失敗*/
static int ds18b20_init(struct ds18b20_dev *dev)
{int ret = 1; // 默認失敗int i;ds18b20_set_input(dev);udelay(10); // 總線穩定時間ds18b20_set_output(dev, LOW); // 拉低復位udelay(500); // >=480usds18b20_set_input(dev); // 釋放總線(高阻)/* 在 15~300us 窗口內輪詢檢測存在脈沖(低電平) */for (i = 0; i < 60; i++) { // 60 * 5us = 300usudelay(5);if (ds18b20_get_io(dev) == LOW) {ret = 0; // 初始化成功,檢測到存在脈沖break;}}/* 等待存在脈沖結束 */udelay(240);ds18b20_set_input(dev); // 保持釋放return ret;
}/** @description : 打開設備* @param - inode : 傳遞給驅動的inode* @param - filp : 設備文件,file結構體有個叫做pr似有ate_data的成員變量* 一般在open的時候將private_data似有向設備結構體。* @return : 0 成功;其他 失敗*/
static int ds18b20_open(struct inode *inode, struct file *filp)
{struct miscdevice *mdev = filp->private_data; /* 由 misc_open 預先設置 */struct ds18b20_dev *ds18b20 = dev_get_drvdata(mdev->this_device);filp->private_data = ds18b20;return 0;
}
/** @description : 從設備讀取數據 * @param - filp : 要打開的設備文件(文件描述符)* @param - buf : 返回給用戶空間的數據緩沖區* @param - cnt : 要讀取的數據長度* @param - offt : 相對于文件首地址的偏移* @return : 讀取的字節數,如果為負值,表示讀取失敗*/
static ssize_t ds18b20_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{struct ds18b20_dev *ds18b20 = filp->private_data;size_t n = 2;if (!ds18b20)return -ENODEV;if (cnt < n)n = cnt;if (copy_to_user(buf, &ds18b20->data[0], n))return -EFAULT;return 0;
}static int ds18b20_release(struct inode *inode, struct file *filp)
{return 0;
}static struct file_operations ds18b20_fops = {.owner = THIS_MODULE,.open = ds18b20_open,.read = ds18b20_read,.release = ds18b20_release,
};/** @description : 使用內核的工作隊列,獲取溫度的原始數據* @param - work : work的結構體* @return : 無*/
static void ds18b20_work_callback(struct work_struct *work)
{int ret;struct ds18b20_dev *dev = container_of(work, struct ds18b20_dev, work);ret = ds18b20_init(dev);if (ret)return;ds18b20_write_byte(dev, 0XCC);ds18b20_write_byte(dev, 0X44);msleep(750);ds18b20_set_input(dev);ret = ds18b20_init(dev);if (ret)return;ds18b20_write_byte(dev, 0XCC);ds18b20_write_byte(dev, 0XBE);dev->data[0] = ds18b20_read_byte(dev);dev->data[1] = ds18b20_read_byte(dev);
}/** @description : 定時器的操作函數,每1s去獲取一次數據* @param - asg : 定時器的結構體* @return : 無*/
/* 定時器回調:每秒觸發一次采集 */
static void ds18b20_timer_callback(struct timer_list *arg)
{struct ds18b20_dev *dev = from_timer(dev, arg, timer);schedule_work(&dev->work);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(1000));
}/*驅動的probe函數,當驅動與設備匹配以后此函數就會執行*/
static int ds18b20_probe(struct platform_device *pdev)
{int ret;struct miscdevice *mdev;struct ds18b20_dev *ds18b20_dev;dev_info(&pdev->dev, "ds18b20 device and driver matched successfully!\n");ds18b20_dev = devm_kzalloc(&pdev->dev, sizeof(*ds18b20_dev), GFP_KERNEL);if (!ds18b20_dev) {return -ENOMEM;}platform_set_drvdata(pdev, ds18b20_dev);/* GPIO的初始化 */ret = ds18b20_request_gpio(pdev);if(ret) {return ret;}mdev = &ds18b20_dev->mdev;mdev->name = "ds18b20";mdev->minor = MISC_DYNAMIC_MINOR;mdev->fops = &ds18b20_fops;ret=misc_register(mdev);if(ret < 0){dev_info(&pdev->dev, "ds18b20 MISC match fail!\n");return -ENODEV;}/* 綁定 drvdata,供 open 通過 mdev->this_device 找回 */if (mdev->this_device)dev_set_drvdata(mdev->this_device, ds18b20_dev);/* 初始化定時器 */timer_setup(&ds18b20_dev->timer, ds18b20_timer_callback, 0);ds18b20_dev->timer.expires=jiffies + msecs_to_jiffies(1000);add_timer(&ds18b20_dev->timer);/* 初始化工作隊列 */INIT_WORK(&ds18b20_dev->work, ds18b20_work_callback);return 0;
}/*驅動的remove函數,移除驅動的時候此函數會執行*/
static int ds18b20_remove(struct platform_device *pdev)
{int ret;struct miscdevice *mdev;struct ds18b20_dev *ds18b20_dev;dev_info(&pdev->dev, "DS18B20 driver has been removed!\n");ds18b20_dev = platform_get_drvdata(pdev);mdev = &ds18b20_dev->mdev;misc_deregister(mdev); /* 卸載定時器 */del_timer(&ds18b20_dev->timer); /* 卸載工作隊列 */cancel_work_sync(&ds18b20_dev->work);return 0;
}static const struct of_device_id ds18b20_of_match[] = {{ .compatible = "alientek,ds18b20" },{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of,ds18b20_of_match);/*platform驅動結構體*/
static struct platform_driver ds18b20_driver = {.driver = {.name = "ds18b20",.of_match_table = ds18b20_of_match,},.probe = ds18b20_probe,.remove = ds18b20_remove,
};/*注冊和注銷一體化*/
module_platform_driver(ds18b20_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("chensir");
MODULE_INFO(intree, "Y");
ds18b20App.c:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"int main()
{int fd, ret;unsigned char result[2];int TH, TL;short tmp = 0;float temperature;int flag = 0;fd = open("/dev/ds18b20", 0);if(fd < 0){perror("open device failed\n");exit(1);}elseprintf("Open success!\n");while(1){ret = read(fd, &result, sizeof(result)); if(ret == 0) { /* 讀取到數據 */TL = result[0];TH = result[1];if((TH == 0XFF) && (TL == 0XFF))/* 如果讀取到數據為0XFFFF就跳出本次循序 */continue;if(TH > 7) { /* 負數處理 */TH = ~TH;TL = ~TL;flag = 1; /* 標記為負數 */}tmp = TH;tmp <<= 8;tmp += TL;if(flag == 1) {temperature = (float)(tmp+1)*0.0625; /* 計算負數的溫度 */temperature = -temperature;}else {temperature = (float)tmp *0.0625; /* 計算正數的溫度 */} if(temperature < 125 && temperature > -55) { /* 溫度范圍 */printf("Current Temperature: %f\n", temperature);}}else if(ret == -1){perror("read"); break;}flag = 0;sleep(1);}close(fd); /* 關閉文件 */
}
makefile:
KERNELDIR := /home/chensir/linux/atk-mp1/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)
obj-m := ds18b20.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
在全部完成之后呢,我又把單總線的GPIO口換成別的地方了,是STM32MP157的PZ6。
需要做以下改動;
在pinctrl-z下添加:
&pinctrl_z {ds18b20_pins: ds18b20-0 {pins1 {pinmux = <STM32_PINMUX('Z', 6, GPIO)>; // PZ6 設為 GPIOdrive-open-drain; // 開漏輸出(釋放=高阻)bias-pull-up; // 上拉(仍建議外部4.7k)slew-rate = <0>;};};
在根節點“/”下追加:
ds18b20@0 {compatible = "alientek,ds18b20";pinctrl-names = "default";pinctrl-0 = <&ds18b20_pins>;ds18b20-gpio = <&gpioz 6 GPIO_ACTIVE_HIGH>;status = "okay";
即可,配置電氣屬性,大家如果想換成別的IO口,就需要知道IO口有沒有被占用,被占用就要解除占用噢!