1.设备树
1.1设备树功能简介
在过去的arm-linux内核源码树中arch/arm/plat-xxx和arch/arm/mach-xxx 等目录下边充斥着大量的垃圾代码,相当多数的代码只是在描述板级细节信息,而这些板级细节信息对于内核来说都是垃圾代码,比如板上的 platform_device、 resource、 i2c_board_info、 spi_board_info 以及各种硬件的 platform_data.
为了改变这种局面,ARM 社区开始在3.10内核后使用 PowerPC 等其他体系架构下已经使用的Flattened Device Tree(FDT). Device Tree 是一种描述硬件的数据结构,它起源于OpenFirmware(OF). 采用 Device Tree后,许多板级细节信息可以直接通过Device Tree传递给 linux内核,而不需要在arch/arm/plat-xxx和arch/arm/mach-xxx中进行大量的冗余编码,内核启动时会展开Device Tree并创建和注册相关的设备(比如platform_device,i2c_client,spi_device),同时驱动程序也会以新的方式和Devie Tree中定义的设备结点进行匹配. Device Tree的主要优势:对于相同SOC的不同板卡,只需更换设备树文件.dtb即可实现不同板卡的无差异支持
system runtime参数(主要设置bootargs)
cpu的数量和类别
memory基地址和大小(设置DDR内存)
总线外设
内核基础设施使用情况,比如:
中断控制器和中断使用情况
DMA控制器和DMA使用情况
GPIO控制器和GPIO使用情况
CLOCK控制器和ClOCK使用情况
PINCTRL控制器和PINCTRL使用情况
1.2设备树的文件类型及编译方式
stm32mp157a-fsmp1a.dts --------------类似于--------->xxx.c
stm32mp15xx-fsmp1x.dtsi --------------类似于--------->xxx.h
|
DTC (make dtbs)
|
stm32mp157a-fsmp1a.dtb 内核能够使用的二进制文件
1.3设备树如何被内核驱动使用
setenv bootcmd tftp 0xc2000040 uImage\;
tftp 0xc4000000 stm32mp157a-fsmp1a.dtb\;
bootm 0xc2000040 - 0xc4000000
在Linux内核启动的时候会解析设备树,会以树状的形式将设备信息存放在内存上。
驱动如果想要使用设备信息,只需要获取到对应的设备树节点,从内部获取设备信息即可。
1.4设备树的语法格式
Device Tree Usage - eLinux.org
1.4.1设备树的组成
设备树是有节点和属性组成的树状结构,属性就是键值对,节点可以包含属性和子节点。
1.4.2设备树的节点的组成
<name>[@<unit-address>
]
name
:节点的类型名,最多不超过31个字符unit_address
:这个是节点内部描述的地址,如果节点内有reg属性就要写地址,如果没有就不写地址
节点特性:
节点可以取别名
demo:mynode@0x12345678{};
节点可以合并
设备树中同名的节点会被合并成一个节点
节点可以被引用
&demo{};
节点中的属性可以被删除
/delete-property/uint;
1.4.3设备树中属性的组成
属性是简单的键值对,其中的值可以为空,也可以包含任意字节流。虽然数据类型没有编码到数据结构中,但是有一些基本的数据表示可以在设备树源文件中表示。
-
文本字符串(以空结尾)用双引号表示:
string-property = "a string";
-
'Cells'是由尖括号分隔的32位无符号整数:
cell-property = <0xbeef 123 0xabcd1234>;
-
二进制数据(16进制表示的单字节的数)用方括号表示:
binary-property = [00 01 23 45 67];
-
不同表示形式的数据可以用逗号连接在一起:
mixed-property = "a string", [01 23 45 67], <0x12345678>;
-
逗号也用于创建字符串列表:
string-list = "red fish", "blue fish";
默认含义的键值对:
compatible = "厂商,设备名";
reg属性
reg是用来描述节点的地址信息的,需要配合#address-cells和#size-cells才能够使用
#address-cells修饰reg一个元素组成中地址成员个数
#size-cells修饰reg一个元素组成中长度个数
1.4.4设备树语法实例
/dts-v1/; //设备树的版本号
/ { // /{}; 根节点
node1 { // node1{}; 根节点的子节点
a-string-property = "A string"; //属性
a-string-list-property = "first string", "second string";
a-byte-data-property = [01 23 34 56];
child-node1 { //node1节点的子节点
first-child-property; //空属性,只起到标识作用
second-child-property = <1>;
a-string-property = "Hello, world";
};
child-node2 { //node1节点的子节点
};
};
node2 { // node2{}; 根节点的子节点
an-empty-property; //空属性,只起到标识作用
a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
child-node1 { //node2节点的子节点
};
};
};
1.4.5设备节点添加
-
在stm32mp157a-fsmp1a.dts添加节点
linux-5.10.61/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts
-
添加的节点如下
mynode@0x12345678{ a-string = "hello DC23041 everyone!"; uint = <0x11223344 0xaabbccdd>; binary-data = [00 0c 29 7b f9 be]; mixed-data = "list",<0x12345678>,[11 22 33]; };
-
编译设备树
make dtbs
-
将设备树文件拷贝到tftpboot目录下
cp arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb ~/tftpboot/
-
重启开发板,让内核解析设备树
-
在/proc/device-tree目录下可以看到添加的设备树节点
1.5驱动如何使用设备信息
1.5.1设备树节点结构体及相关函数
1.节点结构体device_node
struct device_node {
const char *name; //节点名 "mynode"
const char *full_name; //节点全名 "mynode@0x12345678"
struct property *properties; //属性,在一个节点内核所有的属性构成一条单链表
struct device_node *parent; //父节点
struct device_node *child; //子节点
struct device_node *sibling; //兄弟节点
};
2.属性结构体property
struct property {
char *name; //键
int length; //值的字节数
void *value; //值
struct property *next; //指向下一个属性
};
3.获取节点的函数
struct device_node *of_find_node_by_path(const char *path)
功能:通过节点的路径获取节点
参数:
@path:节点的路径 "/mynode@0x12345678"
返回值:成功返回节点的首地址,失败返回NULL
struct device_node *of_find_node_by_name(struct device_node *from,
const char *name)
功能:通过节点的名字获取节点
参数:
@from:填写NULL,表示从根节点开始解析
@name:节点名
返回值:成功返回节点的首地址,失败返回NULL
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type, const char *compatible)
功能:通过compatible获取节点
参数:
@from:NULL,表示从根节点开始解析
@type:NULL
@compatible:厂商和设备名
返回值:成功返回节点的首地址,失败返回NULL
4.获取属性的函数
struct property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
功能:根据属性名获取属性结构体
参数:
@np:节点指针
@name:键
@lenp:获取到的值的长度
返回值:成功返回property结构体指针,失败返回NULL
1.5.2设备树节点获取实例
通过路径名字匹配到节点
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
// mynode@0x12345678{
// a-string = "hello DC23041 everyone!";
// uint = <0x11223344 0xaabbccdd>;
// binary-data = [00 0c 29 7b f9 be];
// mixed-data = "list",<0x12345678>,[11 22 33];
// };
struct device_node* node;
static int __init mynode_init(void)
{
// node = of_find_node_by_path("/mynode@0x12345678");
node = of_find_node_by_name(NULL, "mynode");
if (node == NULL) {
printk("find node failed!\n");
return -ENODATA;
}
printk("key=%s,value=%s\n", node->properties->name, (char*)node->properties->value);
printk("key=%s,value=%#x,%#x\n", node->properties->next->name,
__be32_to_cpup(node->properties->next->value),
__be32_to_cpup(node->properties->next->value+4));
printk("len = %d\n",node->properties->next->length);
return 0;
}
static void __exit mynode_exit(void)
{
}
module_init(mynode_init);
module_exit(mynode_exit);
MODULE_LICENSE("GPL");
通过compatible获取节点
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
// mynode@0x12345678{
// compatible = "hqyj,mynode";
// a-string = "hello DC23041 everyone!";
// uint = <0x11223344 0xaabbccdd>;
// binary-data = [00 0c 29 7b f9 be];
// mixed-data = "list",<0x12345678>,[11 22 33];
// };
struct device_node* node;
static int __init mynode_init(void)
{
node = of_find_compatible_node(NULL, NULL, "hqyj,mynode");
if (node == NULL) {
printk("find node failed!\n");
return -ENODATA;
}
printk("key=%s,value=%s\n", node->properties->name, (char*)node->properties->value);
printk("key=%s,value=%#x,%#x\n", node->properties->next->next->name,
__be32_to_cpup(node->properties->next->next->value),
__be32_to_cpup(node->properties->next->next->value + 4));
return 0;
}
static void __exit mynode_exit(void)
{
}
module_init(mynode_init);
module_exit(mynode_exit);
MODULE_LICENSE("GPL");
通过of_find_property获取属性结构体
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
// mynode@0x12345678{
// compatible = "hqyj,mynode";
// a-string = "hello DC23041 everyone!";
// uint = <0x11223344 0xaabbccdd>;
// binary-data = [00 0c 29 7b f9 be];
// mixed-data = "list",<0x12345678>,[11 22 33];
// };
struct device_node* node;
struct property *prop;
static int __init mynode_init(void)
{
node = of_find_compatible_node(NULL, NULL, "hqyj,mynode");
if (node == NULL) {
printk("find node failed!\n");
return -ENODATA;
}
prop = of_find_property(node, "uint", NULL);
if (prop == NULL) {
printk("find property failed!\n");
return -ENODATA;
}
printk("key: %s, value: %#x\n", prop->name,__be32_to_cpup(prop->value));
return 0;
}
static void __exit mynode_exit(void)
{
}
module_init(mynode_init);
module_exit(mynode_exit);
MODULE_LICENSE("GPL");
2.gpio子系统
2.1gpio子系统框架
2.2gpio子系统的API
int of_get_named_gpio(struct device_node *np,
const char *propname, int index)
功能:解析设备树上的属性对应的gpio号
参数:
@np:节点的指针
@propname:键
@index:索引号
返回值:成功返回gpio号,失败返回错误码
int gpio_request(unsigned gpio, const char *label)
功能:申请要使用的gpio
参数:
@gpio:gpio编号(设备树)
@label:名字(NULL)
返回值:成功返回0,失败返回错误码
int gpio_direction_input(unsigned gpio)
功能:设置管脚的方向为输入
参数:
@gpio:gpio编号
返回值:成功返回0,失败返回错误码
int gpio_direction_output(unsigned gpio, int value)
功能:设置管脚的方向为输出
参数:
@gpio:gpio编号
@value:1高 0低电平
返回值:成功返回0,失败返回错误码
void gpio_set_value(unsigned gpio, int value)
功能:设置电平的状态
参数:
@gpio:gpio编号
@value:1高 0低电平
返回值:无
int gpio_get_value(unsigned gpio)
功能:获取电平的状态
参数:
@gpio:gpio编号
返回值:1高电平,0低电平
void gpio_free(unsigned gpio)
功能:释放gpio
参数:
@gpio:gpio编号
返回值:无
2.3gpio子系统的设备树
2.3.1画出硬件连接原理图
2.3.2找出控制器的设备树
arch/arm/boot/dts/stm32mp151.dtsi
gpioe: gpio@50006000 {
gpio-controller; //空属性,标识当前的节点是gpio控制器的节点
#gpio-cells = <2>; //修饰子节点使用当前节点成员个数是2
reg = <0x50006000 0x400>; //控制器地址及长度
clocks = <&rcc GPIOE>; //时钟
st,bank-name = "GPIOE"; //GPIOE节点
status = "disabled"; //控制器状态 "disabled"没有使能 "okay"使能
}; //控制器已经使能了
2.3.3参考内核帮助文档编写自己的设备树
/home/linux/linux-5.10.61/Documentation/devicetree/bindings/gpio/$ vi gpio.txt
myleds{
led1 = <&gpioe 10 0>;
led2 = <&gpiof 10 0>;
led3 = <&gpioe 8 0>;
};
2.4gpio子系统点灯实例
思路:
1.在~/linux-5.10.61/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts下添加myleds节点
2.编写驱动文件
init:
i.获取节点
node=of_fine_node_path()
ii.解析获取gpio号
gpiono=of_get_named_qpio()
iii.申请gpio
ret=gpio_request()
iv.设置gpio输出(可以直接在这一步设置高低电平)
ret=gpio_direction_output()
v.设置高电平
gpio_set_value()
exit:
关灯
释放gpio资源
注意别写错设备树文件,不然报错
myleds{
led1=<&gpioe,10,0>;
led2=<&gpiof,10,0>;
led3=<&gpioe,8,0>;
};
myled.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
//1.添加设备节点
// myleds{
// led1=<&gpioe,10,0>;
// led2=<&gpiof,10,0>;
// led3=<&gpioe,8,0>;
// }
//2.1创建节点结构体
struct device_node *node;
//3.1
int gpiono1,gpiono2,gpiono3,ret;
static int __init myled_init(void){
//2.获取节点
if(NULL==(node=of_find_node_by_path("/myleds"))){
printk("of_find_node_by_path error\n");
return -ENODATA;
}
//3.解析gpio号
//led1
if(0>(gpiono1=of_get_named_gpio(node,"led1",0))){
printk("of_get_named_gpio error\n");
return gpiono1;
}
//led2
if(0>(gpiono2=of_get_named_gpio(node,"led2",0))){
printk("of_get_named_gpio error\n");
return gpiono2;
}
//led3
if(0>(gpiono3=of_get_named_gpio(node,"led3",0))){
printk("of_get_named_gpio error\n");
return gpiono3;
}
//3.申请gpio
ret=gpio_request(gpiono1,NULL);
if(ret){
printk("gpio_request error\n");
return -ret;
}
ret=gpio_request(gpiono2,NULL);
if(ret){
printk("gpio_request error\n");
return -ret;
}
ret=gpio_request(gpiono3,NULL);
if(ret){
printk("gpio_request error\n");
return -ret;
}
//4.设置gpio输出模式
//点亮三个灯
ret=gpio_direction_output(gpiono1,1);
if(ret){
printk("gpio_direction_output error\n");
return ret;
}
ret=gpio_direction_output(gpiono2,1);
if(ret){
printk("gpio_direction_output error\n");
return ret;
}
ret=gpio_direction_output(gpiono3,1);
if(ret){
printk("gpio_direction_output error\n");
return ret;
}
return 0;
}
static void __exit myled_exit(void){
//关灯
gpio_set_value(gpiono1,0);//led1
gpio_set_value(gpiono2,0);//led2
gpio_set_value(gpiono3,0);//led3
//释放gpio
gpio_free(gpiono1);
gpio_free(gpiono2);
gpio_free(gpiono3);
}
module_init(myled_init);
module_exit(myled_exit);
MODULE_LICENSE("GPL");
常用方法
myleds.h
#ifndef __MYLEDS_H__
#define __MYLEDS_H__
#define LED_ON _IO('o',0)
#define LED_OFF _IO('o',1)
#endif
myleds.c
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include "myleds.h"
// myleds{
// led1 = <&gpioe 10 0>;
// led2 = <&gpiof 10 0>;
// led3 = <&gpioe 8 0>;
// };
#define CNAME "myleds"
int major = 0;
struct class* cls;
struct device* dev;
struct device_node* node;
int gpiono;
// open
int myleds_open(struct inode* inode, struct file* filp)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
// ioctl
long myleds_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
switch(cmd){
case LED_ON:
gpio_set_value(gpiono, 1);
break;
case LED_OFF:
gpio_set_value(gpiono, 0);
break;
}
return 0;
}
// close
int myleds_close(struct inode* inode, struct file* filp)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
struct file_operations fops = {
.open = myleds_open,
.unlocked_ioctl = myleds_ioctl,
.release = myleds_close,
};
static int __init myleds_init(void)
{
int ret;
// 1.获取节点
node = of_find_node_by_path("/myleds");
if (node == NULL) {
printk("find node failed!\n");
ret = -ENODATA;
goto err1;
}
// 2.解析得到gpio号
gpiono = of_get_named_gpio(node, name[i], 0);
if (gpiono < 0) {
printk("get gpio failed!\n");
ret = gpiono;
goto err1;
}
// 3.申请gpio
ret = gpio_request(gpiono, NULL);
if (ret) {
printk("request gpio failed!\n");
goto err1;
}
// 4.设置gpio为输出
ret = gpio_direction_output(gpiono, 0);
if (ret) {
printk("set gpio direction failed!\n");
goto err2;
}
// 5.注册字符设备驱动
major = register_chrdev(0, CNAME, &fops);
if (major < 0) {
printk("register_chrdev failed!\n");
goto err2;
}
// 6.创建设备节点
cls = class_create(THIS_MODULE, CNAME);
if (IS_ERR(cls)) {
printk("class_create failed!\n");
ret = PTR_ERR(cls);
goto err3;
}
dev = device_create(cls, NULL, MKDEV(major, 0), NULL, CNAME);
if (IS_ERR(dev)) {
printk("device_create failed!\n");
ret = PTR_ERR(dev);
goto err4;
}
return 0;
err4:
class_destroy(cls);
err3:
unregister_chrdev(major, CNAME);
err2:
gpio_free(gpiono);
err1:
return ret;
}
static void __exit myleds_exit(void)
{
device_destroy(cls, MKDEV(major, 0));
class_destroy(cls);
unregister_chrdev(major, CNAME);
gpio_set_value(gpiono, 0);
gpio_free(gpiono);
}
module_init(myleds_init);
module_exit(myleds_exit);
MODULE_LICENSE("GPL");
test.c
#include "myleds.h"
#include <head.h>
#include <sys/ioctl.h>
int main(int argc, const char* argv[])
{
int fd;
if ((fd = open("/dev/myleds", O_RDWR)) == -1)
PRINT_ERR("open error");
while (1) {
if (ioctl(fd, LED_ON))
PRINT_ERR("ioctl error");
sleep(1);
if (ioctl(fd, LED_OFF))
PRINT_ERR("ioctl error");
sleep(1);
}
close(fd);
return 0;
}
2.5gpio子系统的练习
-
练习:使用gpio子系统将开发板上的六盏灯点亮
myleds{ core_leds{ led1 = <&gpioz 5 0>; led2 = <&gpioz 6 0>; led3 = <&gpioz 7 0>; }; extend_leds{ led1 = <&gpioe 10 0>; led2 = <&gpiof 10 0>; led3 = <&gpioe 8 0>; }; };
3.modprobe安装命令使用方法
3.1在内核目录下执行如下命令
make modules_install INSTALL_MOD_PATH=/home/linux/rootfs
3.2修改Makefile
arch?=arm
modname?=demo
ifeq ($(arch),arm)
KERNELDIR:= /home/linux/linux-5.10.61
else
KERNELDIR := /lib/modules/$(shell uname -r)/build/
endif
PWD := $(shell pwd)
all:
make -C $(KERNELDIR) M=$(PWD) modules
install:
make -C $(KERNELDIR) M=$(PWD) modules_install INSTALL_MOD_PATH=/home/linux/rootfs
clean:
make -C $(KERNELDIR) M=$(PWD) clean
obj-m:= $(modname).o #编译编译的模块是demo
make install
模块会被拷贝到开发板/lib/modules/5.10.61/extra/目录下
3.3安装卸载模块的命令
安装命令:modprobe 模块名
卸载命令:modprobe -r 模块名
modprobe和insmod的区别?
- insmod安装模块的时候不会安装依赖模块,modprobe会安装依赖模块
- insmod安装的时候可以指定模块路径安装,modprobe安装模块的时候都是/lib/modules/5.10.61/kernel extra
4.Linux内核中的中断子系统
4.1异常处理流程
中断是基于硬件实现的,不管有没有Linux内核中断的执行都是一样的,中断
是异常的一种,遵从4大步3小步的执行流程。
- 将cpsr保存spsr中
- 修改cpsr的值
- 设置为ARM模型
- 设置为对应的异常模式
- 如果有必要的话就进制中断
- 将LR的值保存起来(返回的地址)
- 设置PC跳转到对应的位置执行
4.2Linux内核中断处理流程
4.3Linux内核中断子系统API
#include <linux/interrupt.h>
#include <linux/of_irq.h>
unsigned int irq_of_parse_and_map(struct device_node *np, int index)
功能:解析得到软中断号
参数:
@np:节点指针
@index:设备树节点中interrupts键对应值的下标
返回值:成功返回软中断号,失败返回0
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
功能:注册中断
参数:
@irq:软中断号(设备树)
@handler:中断处理函数指针
//typedef irqreturn_t (*irq_handler_t)(int, void *);
irqreturn_t key_irq_handle(int irqno,void *dev)
{
//中断的处理
return IRQ_NONE; //失败
return IRQ_HANDLED; //成功
}
@flags:中断触发方式
IRQF_TRIGGER_RISING //上升沿
IRQF_TRIGGER_FALLING //下降沿
IRQF_TRIGGER_HIGH //高电平
IRQF_TRIGGER_LOW //低电平
IRQF_SHARED //共享中断
@name:中断的名字
cat /proc/interrupts
@dev:给中断处理函数传递的参数
返回值:成功返回0,失败返回错误码
const void *free_irq(unsigned int irq, void *dev_id)
功能:释放中断
参数:
@irq:软中断号
@dev_id:给中断处理函数传递的参数
返回值:返回设备的名字
4.4Linux内核中断子系统设备树填写
4.4.1画出硬件连接原理图
4.4.2找出控制器的设备树
stm32mp151.dtsi
intc: interrupt-controller@a0021000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>; //修饰子节点使用当前节点成员个数(中断)
interrupt-controller;
reg = <0xa0021000 0x1000>,
<0xa0022000 0x2000>;
};
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>; //父节点
exti: interrupt-controller@5000d000 {
compatible = "st,stm32mp1-exti", "syscon";
interrupt-controller;
#interrupt-cells = <2>;
reg = <0x5000d000 0x400>;
};
};
pinctrl: pin-controller@50002000 {
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&exti>; //父节点
gpiof: gpio@50007000 {
interrupt-controller;
#interrupt-cells = <2>; //修饰子节点使用当前节点成员个数(中断)
reg = <0x50007000 0x400>;
clocks = <&rcc GPIOF>;
st,bank-name = "GPIOF";
status = "disabled"; // 控制器已经使能了
};
};
4.4.3参考内核帮助文档编写自己的设备树
/home/linux/linux-5.10.61/Documentation/devicetree/bindings/interrupt-controller/$
vi interrupts.txt
myirqs{
interrupt-parent = <&gpiof>; //父节点
interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
};
1.5中断子系统实例(一个按键)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
// myirqs{
// interrupt-parent = <&gpiof>; //父节点
// interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
// };
struct device_node *node;
unsigned int irqno;
irqreturn_t key_irq_handle(int irq, void *dev)
{
printk("key1 down...\n");
return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
int ret;
// 1.获取节点
node = of_find_node_by_path("/myirqs");
if (node == NULL)
{
printk("of_find_node_by_path error\n");
return -EINVAL;
}
// 2.获取软中断号
irqno = irq_of_parse_and_map(node, 0);
if(irqno == 0)
{
printk("irq_of_parse_and_map error\n");
return -EINVAL;
}
// 3.注册中断
ret = request_irq(irqno, key_irq_handle, IRQF_TRIGGER_FALLING, "myirqs", NULL);
if(ret){
printk("request_irq error\n");
return ret;
}
return 0;
}
static void __exit myirqs_exit(void)
{
free_irq(irqno, NULL);
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");
4.6中断子系统实例(多个按键)
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
// myirqs{
// interrupt-parent = <&gpiof>; //父节点
// interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
// };
struct device_node* node;
unsigned int irqno[3];
irqreturn_t key_irq_handle(int irq, void* dev)
{
switch ((int)dev) {
case 0:
printk("key1 down...\n");
break;
case 1:
printk("key2 down...\n");
break;
case 2:
printk("key3 down...\n");
break;
}
return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
int ret, i;
// 1.获取节点
node = of_find_node_by_path("/myirqs");
if (node == NULL) {
printk("of_find_node_by_path error\n");
return -EINVAL;
}
for (i = 0; i < ARRAY_SIZE(irqno); i++) {
// 2.获取软中断号
irqno[i] = irq_of_parse_and_map(node, i);
if (irqno[i] == 0) {
printk("irq_of_parse_and_map error\n");
return -EINVAL;
}
// 3.注册中断
ret = request_irq(irqno[i], key_irq_handle,
IRQF_TRIGGER_FALLING, "myirqs", (void*)i);
if (ret) {
printk("request_irq error\n");
return ret;
}
}
return 0;
}
static void __exit myirqs_exit(void)
{
int i;
for (i = 0; i < ARRAY_SIZE(irqno); i++) {
free_irq(irqno[i], (void*)i);
}
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");
5.内核定时器
5.1内核定时器定时原理
-
内核当前时间如何获取?
jiffies:内核时钟节拍数,从系统启动这一刻起这个jiffies(64bit)值就一直在增加。jiffies可以直接使用
不需要包含头文件。
-
定时器每增加1走的时间?
定时器的频率可以通过make menuconfig选配,选配后的选项在.config文件中保存着。
开发板:CONFIG_HZ=100就是频率,定时器每增加1代表走10ms。
Ubuntu:CONFIG_HZ=250就是频率,定时器每增加1代表走4ms。
5.2内核定时器的API
1.分配对象
struct timer_list {
struct hlist_node entry; //构成内核链表成员
unsigned long expires; //定时器时间
void (*function)(struct timer_list *); //定时器处理函数
u32 flags; //标志位,一般写0
};
struct timer_list mytimer;
2.对象的初始化
mytimer.expires = jiffies + HZ; //定时1s #define HZ CONFIG_HZ
timer_setup(&mytimer, 定时器处理函数, 0);
3.启动定时器
void add_timer(struct timer_list *timer);
//启动定时器,定时器启动之后只会执行一次
mod_timer(struct timer_list *timer, unsigned long expires);
//再次启动定时器
4.删除定时器
int del_timer(struct timer_list * timer);
//移除定时器
5.3内核定时器的实例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
// myleds
// {
// led1 = <&gpioe 10 0>;
// led2 = <&gpiof 10 0>;
// led3 = <&gpioe 8 0>;
// };
struct timer_list mytimer;
struct device_node* node;
int gpiono[3];
char* name[] = { "led1", "led2", "led3" };
// 定时器处理函数
void timer_led_handle(struct timer_list* timer)
{
int i;
for (i = 0; i < ARRAY_SIZE(name); i++) {
gpio_set_value(gpiono[i],!gpio_get_value(gpiono[i]));
}
mod_timer(&mytimer, jiffies + HZ);
}
static int __init mytimer_init(void)
{
int ret, i;
// 1.获取节点
node = of_find_node_by_name(NULL, "myleds");
if (node == NULL) {
printk("of_find_node_by_name error\n");
return -EINVAL;
}
for (i = 0; i < ARRAY_SIZE(name); i++) {
// 2.解析gpio号
gpiono[i] = of_get_named_gpio(node, name[i], 0);
if (gpiono[i] < 0) {
printk("of_get_named_gpio error\n");
return -EINVAL;
}
// 3.初始化LED
ret = gpio_request(gpiono[i], name[i]);
if (ret) {
printk("gpio_request error\n");
return ret;
}
ret = gpio_direction_output(gpiono[i], 0);
if (ret) {
printk("gpio_direction_output error\n");
return ret;
}
}
// 4.初始化定时器
mytimer.expires = jiffies + HZ;
timer_setup(&mytimer, timer_led_handle, 0);
// 5.启动定时器
add_timer(&mytimer);
return 0;
}
static void __exit mytimer_exit(void)
{
int i;
del_timer(&mytimer);
for (i = 0; i < ARRAY_SIZE(name); i++) {
gpio_free(gpiono[i]);
}
}
module_init(mytimer_init);
module_exit(mytimer_exit);
MODULE_LICENSE("GPL");
5.4内核定时器的练习
-
使用内核定时器让LED闪烁
-
使用内核定时器完成按键中断消抖工作
#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
// 三个按键中断并消抖:中断 定时器
struct device_node *node;
struct timer_list mytimer;
unsigned int irq[3];
int gpiono[3];
void timer_handle(struct timer_list *tim)
{
// 按键按下后执行对应操作
int i;
for (i = 0; i < 3; i++)
{
if (!gpio_get_value(gpiono[i]))
{
printk("key%d down...\n", gpiono[i]);
}
}
}
irqreturn_t key_irq_handle(int irq, void *dev)
{
// 执行定时器函数
mod_timer(&mytimer, jiffies + 1); // 再次启动定时器
return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
int ret, i;
// 1.获取节点
if (NULL == (node = of_find_node_by_path("/myirqs")))
{
printk("of_find_get_node error\n");
return -EINVAL;
}
// printk("111111111111111\n");
// 4.初始化定时器
mytimer.expires = jiffies + 1; // 10ms
timer_setup(&mytimer, timer_handle, 0);
add_timer(&mytimer);
for (i = 0; i < 3; i++)
{
// 2.获取中断号
if (0 == (irq[i] = irq_of_parse_and_map(node, i)))
{
printk("of_find_get_node error\n");
return ret;
}
// 3.获取gpio号
if (0 > (gpiono[i] = of_get_named_gpio(node, "keys", i)))
{
printk("of_get_named_gpio error\n");
return gpiono[i];
}
// printk("22222222222222222\n");
// 5.注册中断
if (0 != (ret = request_irq(irq[i], key_irq_handle, IRQF_TRIGGER_FALLING, "myirqs", NULL)))
{
printk("request_irq error\n");
return ret;
}
}
return 0;
}
static void __exit myirqs_exit(void)
{
int i;
for (i = 0; i < 3; i++)
{
free_irq(irq[i], NULL);
}
del_timer(&mytimer);
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");
错误:有的时候会遇到这个问题,有的时候就能运行成功,原因是将第四步初始化定时器写在了for循环内,现在将定时器拿出来就没有问题了,写的时候需注意
6.输入子系统驱动
6.1输入子系统驱动简介
输入类设备都是通过输入子系统进行数据上报的。比如鼠标,键盘,触摸屏等驱动
都是需要通过输入子系统驱动上报数据的,上报的数据是input_event
结构体,这个
input_event结构体被上报/dev/input/event*
节点中了。
6.2输入子系统驱动框架结构
6.3输入子系统驱动API
~/linux-5.10.61/Documentation$ vi input/input-programming.rst
1.分配对象
struct input_dev *key_dev;
key_dev = input_allocate_device();
2.对象初始化
set_bit(EV_KEY, key_ev->evbit); // 指定上报的是键盘类事件
set_bit(KEY_L, key_ev->keybit); // 指定上报l键
set_bit(KEY_S, key_ev->keybit); // 指定上报s键
set_bit(KEY_ENTER, key_ev->keybit); // 指定上报enter键
3.注册
int input_register_device(struct input_dev *dev)
4.注销
void input_unregister_device(struct input_dev *dev)
5.上报数据
void input_event(struct input_dev *dev,
unsigned int type, unsigned int code, int value) //上报数据
void input_sync(struct input_dev *dev) //同步
6.4键盘驱动的实例
keybroad.c
#include <linux/init.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
// myirqs{
// interrupt-parent = <&gpiof>; //父节点
// interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
// };
struct device_node* node;
unsigned int irqno[3];
struct input_dev* key_ev;
irqreturn_t key_irq_handle(int irq, void* dev)
{
switch ((int)dev) {
case 0:
input_event(key_ev, EV_KEY, KEY_L, 1);
input_sync(key_ev);
input_event(key_ev, EV_KEY, KEY_L, 0);
input_sync(key_ev);
break;
case 1:
input_event(key_ev, EV_KEY, KEY_S, 1);
input_sync(key_ev);
input_event(key_ev, EV_KEY, KEY_S, 0);
input_sync(key_ev);
break;
case 2:
input_event(key_ev, EV_KEY, KEY_ENTER, 1);
input_sync(key_ev);
input_event(key_ev, EV_KEY, KEY_ENTER, 0);
input_sync(key_ev);
break;
}
return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
int ret, i;
// 1.获取节点
node = of_find_node_by_path("/myirqs");
if (node == NULL) {
printk("of_find_node_by_path error\n");
return -EINVAL;
}
for (i = 0; i < ARRAY_SIZE(irqno); i++) {
// 2.获取软中断号
irqno[i] = irq_of_parse_and_map(node, i);
if (irqno[i] == 0) {
printk("irq_of_parse_and_map error\n");
return -EINVAL;
}
// 3.注册中断
ret = request_irq(irqno[i], key_irq_handle,
IRQF_TRIGGER_FALLING, "myirqs", (void*)i);
if (ret) {
printk("request_irq error\n");
return ret;
}
}
// 4.注册输入子系统
key_ev = input_allocate_device();
if (key_ev == NULL) {
printk("input_allocate_device error\n");
return -ENOMEM;
}
set_bit(EV_KEY, key_ev->evbit); // 指定上报的是键盘类事件
set_bit(KEY_L, key_ev->keybit); // 指定上报l键
set_bit(KEY_S, key_ev->keybit); // 指定上报s键
set_bit(KEY_ENTER, key_ev->keybit); // 指定上报s键
ret = input_register_device(key_ev);
if (ret) {
printk("input_register_device error\n");
return ret;
}
return 0;
}
static void __exit myirqs_exit(void)
{
int i;
input_unregister_device(key_ev);
for (i = 0; i < ARRAY_SIZE(irqno); i++) {
free_irq(irqno[i], (void*)i);
}
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");
test.c
可以使用如下程序测试键盘上报的数据,也可以通过exec 0</dev/tty1
#include <head.h>
#include <linux/input.h>
#include <sys/select.h>
int main(int argc, const char* argv[])
{
int fd1, fd2, ret;
fd_set rfds;
struct input_event key_ev;
char buf[128] = { 0 };
if ((fd2 = open("/dev/input/event0", O_RDWR)) == -1)
PRINT_ERR("open keybroad error");
while (1) {
FD_ZERO(&rfds);
FD_SET(fd2, &rfds);
if ((ret = select(fd2 + 1, &rfds, NULL, NULL, NULL)) == -1)
PRINT_ERR("select error");
if (FD_ISSET(fd2, &rfds)) {
memset(&key_ev, 0, sizeof(key_ev));
read(fd2, &key_ev, sizeof(key_ev));
printf("type = %d,code = %d,value=%d\n",
key_ev.type,key_ev.code,key_ev.value);
}
}
close(fd2);
return 0;
}
7.中断底半部
7.1中断底半部的简介
什么中断底半部?为什么要设计中断底半部?
中断顶半部不能做延时,耗时,甚至休眠的操作,也就是说在中断顶半部中只能够
做简短的、紧急的、不耗时的操作。但是有的时候又希望在中断到来的时候做尽可能
多的操作,所以两者就产生了矛盾,内核为了解决这一矛盾引入了中断底半部的机制。
也就是在中断底半部中,可以做不紧急的,耗时的操作。中断底半部的机制分为软中
断、tasklet、工作队列。
eg:
在网卡中断到来的时候,想要在中断处理函数中接收网络数据包,这个过程就是耗时
的操作,所以内核将这一操作放在中中断底半部机制中(软中断)。
7.2中断底半部之tasklet
7.2.1tasklet的工作原理
tasklet是基于软中断实现的,特点和软中断一样,但是tasklet没有个数限制,而软中断
是有个数限制的,因为tasklet是通过链表来实现的。tasklet这种中断底半部的机制工作
在中断上下文,它是中断的一个部分,不能够脱离中断单独执行,在tasklet的底半部中
不能够做休眠的操作,只能做相对耗时或延时的操作。tasklet这种底半部的机制在中断
顶半部执行即将结束的时候被调用执行的。
7.2.2tasklet的API
1.分配对象
struct tasklet_struct
{
struct tasklet_struct *next; //构成tasklet的链表
unsigned long state; //是否被触发的标志位
atomic_t count; //触发的次数
bool use_callback; //如果为真使用callback,如果为假使用func
union {
void (*func)(unsigned long data); //旧版本接口
void (*callback)(struct tasklet_struct *t); //新版本接口
};
unsigned long data; //向底半部处理函数传递的参数
};
struct tasklet_struct tasklet;
2.初始化对象
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data) //旧版本的初始化
void tasklet_setup(struct tasklet_struct *t,
void (*callback)(struct tasklet_struct *)) //新版本的初始化
3.开启底半部
void tasklet_schedule(struct tasklet_struct *t)
7.2.3tasklet的代码实例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
struct device_node *node;
struct tasklet_struct tasklet;
int irqno;
void key_irq_tasklet(struct tasklet_struct *task){
int i=10;
while(i--){
printk("key_irq_tasklet i = %d\n",i);
mdelay(500); //延时500ms
}
}
irqreturn_t key_irq_handle(int irq, void *dev){
// 5.开启底半部
tasklet_schedule(&tasklet);
return IRQ_HANDLED;
}
static int __init myirqs_init(void){
int ret;
//1.获取节点
if (NULL == (node = of_find_node_by_path("/myirqs")))
{
printk("of_find_get_node error\n");
return -EINVAL;
}
// 2.获取中断号
if (0 == (irqno= irq_of_parse_and_map(node, 0)))
{
printk("of_find_get_node error\n");
return ret;
}
//3.初始化myirqs
tasklet_setup(&tasklet,key_irq_tasklet);
// 4.注册中断
if (0 != (ret = request_irq(irqno, key_irq_handle, IRQF_TRIGGER_FALLING, "myirqs", NULL)))
{
printk("request_irq error\n");
return ret;
}
return 0;
}
static void __exit myirqs_exit(void){
free_irq(irqno, NULL);
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");
7.3中断底半部之工作队列
7.3.1工作队列的工作原理
在内核启动的时候默认会启动一个events的线程,这个线程默认处于休眠的状态,在这个
线程中维护了一个队列,如果你想让这个线程执行你的代码,你只需要向这个线程工作队列
中提交一个队列项,然后唤醒这个休眠的线程执行你的代码即可。工作队列工作在进程上下
文。工作队列没有个数限制,可以脱离中断单独执行。在工作队列中可以执行延时,耗时,
甚至休眠的操作。
7.3.2工作队列的API
1.分配对象
struct work_struct {
atomic_long_t data; //向底半部处理函数传递的的数据
struct list_head entry; //队列项
work_func_t func; //底半部处理函数
//typedef void (*work_func_t)(struct work_struct *work);
}
struct work_struct work;
2.对象的初始化
INIT_WORK(&work, 工作队列底半部处理函数);
3.调用执行
schedule_work(struct work_struct *work)
4.等待工作队列执行完在卸载驱动
cancel_work_sync(struct work_struct *work)
7.3.3工作队列的代码实例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
8.platform总线驱动
8.1device、bus、driver模型
在Linux内核中所有总线驱动都遵从设备驱动的模型,总线驱动的模型如下图:
内核在设计这些总线驱动模型的时候将一个驱动分为了三个部分device、bus、driver。device是用来描述硬件设备的,bus是总线用来链接device和driver,driver是用来描述驱动的对象。在内核中所有的device放在内核的klist_devices的链表中管理,而内核中所有的driver放在klist_driver中管理。内核中的device和driver通过bus完成关联。当device和driver匹配成功之后执行驱动的probe函数,在probe函数中就可以完成操作硬件了。当卸载任何一方驱动的时候都会执行驱动中的remove函数。
8.2platform总线驱动的原理
platform总线驱动遵从设备模型,platform是Linux内核抽象出来的软件代码,并没有真实的总线协议与之对应。platform总线驱动的思想就是要将设备信息和设备驱动进行分离。platform_device和platform_driver通过总线匹配成功之后会执行驱动中probe函数,在probe函数中驱动就能够拿到设备信息。
8.3platform总线驱动的API
1.platform_device端
1.1分配对象初始化
struct platform_device {
const char *name; //用于匹配的名字
int id; //总线号 PLATFORM_DEVID_AUTO
struct device dev; //父类
u32 num_resources; //设备信息成员个数
struct resource *resource; //设备信息结构体
};
struct device{ //父类
void (*release)(struct device *dev);
};
struct resource {
resource_size_t start; //起始资源 0x50006000 0x12345678 71
resource_size_t end; //结束资源 0x50006000+3 0x12345678 +49 71
unsigned long flags; //资源类型 IORESOURCE_IO IORESOURCE_MEM IORESOURCE_IRQ
};
1.2注册
int platform_device_register(struct platform_device *);
1.3注销
void platform_device_unregister(struct platform_device *);
2.platform_driver端
1.分配并初始化对象
struct platform_driver {
int (*probe)(struct platform_device *); //匹配成功执行的函数
int (*remove)(struct platform_device *);//分离的时候执行的函数
struct device_driver driver; //父类
const struct platform_device_id *id_table; //2idtable匹配
};
struct device_driver {//父类
const char *name; //1按照名字匹配
const struct of_device_id *of_match_table; //3.设备树匹配
}
2.注册
platform_driver_register(drv)
3.注销
void platform_driver_unregister(struct platform_driver *);
4.一键注册注销的宏
module_platform_driver(变量名);
8.4platform总线驱动的实例(name)
pdev.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
struct resource res[] = {
[0] = {
.start = 0x12345678,
.end = 0x12345678+49,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 71,
.end = 71,
.flags = IORESOURCE_IRQ,
},
};
void pdev_release(struct device *dev)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
struct platform_device pdev = {
.name = "duang duang duang",
.id = PLATFORM_DEVID_AUTO,
.dev = {
.release = pdev_release,
},
.resource = res,
.num_resources = ARRAY_SIZE(res),
};
static int __init pdev_init(void)
{
return platform_device_register(&pdev);
}
static void __exit pdev_exit(void)
{
platform_device_unregister(&pdev);
}
module_init(pdev_init);
module_exit(pdev_exit);
MODULE_LICENSE("GPL");
pdrv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
int pdrv_probe(struct platform_device *pdev)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
int pdrv_remove(struct platform_device *pdev)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
struct platform_driver pdrv = {
.probe = pdrv_probe,
.remove = pdrv_remove,
.driver = {
.name = "duang duang duang",
},
};
static int __init pdrv_init(void)
{
return platform_driver_register(&pdrv);
}
static void __exit pdrv_exit(void)
{
platform_driver_unregister(&pdrv);
}
module_init(pdrv_init);
module_exit(pdrv_exit);
MODULE_LICENSE("GPL");
8.5platform驱动获取设备信息的过程
struct resource *platform_get_resource(struct platform_device *pdev,
unsigned int type, unsigned int index)
功能:在platform驱动中获取设备信息
参数:
@pdev:platform_device的结构体指针
@type:类型 IORESOURCE_IO IORESOURCE_MEM IORESOURCE_IRQ
@index:同类型资源的下标
返回值:成功返回结构体指针,失败返回NULL
int platform_get_irq(struct platform_device *pdev, unsigned int index)
功能:获取中断类型的资源
参数:
@pdev:platform_device的结构体指针
@index:中断类型资源的下标
返回值:成功返回中断号,失败返回错误码
pdev.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
struct resource res[] = {
[0] = {
.start = 0x12345678,
.end = 0x12345678 + 49,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 71,
.end = 71,
.flags = IORESOURCE_IRQ,
},
};
void pdev_release(struct device* dev)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
struct platform_device pdev = {
.name = "duang duang duang",
.id = PLATFORM_DEVID_AUTO,
.dev = {
.release = pdev_release,
},
.resource = res,
.num_resources = ARRAY_SIZE(res),
};
static int __init pdev_init(void)
{
return platform_device_register(&pdev);
}
static void __exit pdev_exit(void)
{
platform_device_unregister(&pdev);
}
module_init(pdev_init);
module_exit(pdev_exit);
MODULE_LICENSE("GPL");
pdrv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
struct resource *res;
int irqno;
int pdrv_probe(struct platform_device *pdev)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
res = platform_get_resource(pdev,IORESOURCE_MEM,0);
if(res == NULL){
printk("platform_get_resource error\n");
return -EINVAL;
}
irqno = platform_get_irq(pdev,0);
if(irqno < 0){
printk("platform_get_irq error\n");
return -EINVAL;
}
printk("addr = %#llx,irqno = %d\n",res->start,irqno);
return 0;
}
int pdrv_remove(struct platform_device *pdev)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
struct platform_driver pdrv = {
.probe = pdrv_probe,
.remove = pdrv_remove,
.driver = {
.name = "duang duang duang",
},
};
static int __init pdrv_init(void)
{
return platform_driver_register(&pdrv);
}
static void __exit pdrv_exit(void)
{
platform_driver_unregister(&pdrv);
}
module_init(pdrv_init);
module_exit(pdrv_exit);
MODULE_LICENSE("GPL");
8.6platform驱动idtable匹配方式
8.6.1为什么需要idtable匹配方式
如果按照name匹配,driver只能对应一个device,但是有的时候某些厂商
生成的一系列设备为这一系列的设备写一个驱动,所以此时驱动中就需要把
它兼容的设备名字全部写上去,保存这些名字的结构体就叫做idtable。
8.6.2idtable匹配方式的实例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
struct resource* res;
int irqno;
int pdrv_probe(struct platform_device* pdev)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
printk("name = %s,driver_data = %ld\n",
pdev->id_entry->name,pdev->id_entry->driver_data);
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res == NULL) {
printk("platform_get_resource error\n");
return -EINVAL;
}
irqno = platform_get_irq(pdev, 0);
if (irqno < 0) {
printk("platform_get_irq error\n");
return -EINVAL;
}
printk("addr = %#llx,irqno = %d\n", res->start, irqno);
return 0;
}
int pdrv_remove(struct platform_device* pdev)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
// idtable匹配方式
const struct platform_device_id idtable[] = {
{"hello0",111},
{"hello1",222},
{"hello2",333},
{},
};
struct platform_driver pdrv = {
.probe = pdrv_probe,
.remove = pdrv_remove,
.driver = {
.name = "duang duang duang",
},
.id_table = idtable,
};
static int __init pdrv_init(void)
{
return platform_driver_register(&pdrv);
}
static void __exit pdrv_exit(void)
{
platform_driver_unregister(&pdrv);
}
module_init(pdrv_init);
module_exit(pdrv_exit);
MODULE_LICENSE("GPL");
8.7platform驱动设备树匹配方式
在有设备树的内核版本中会将platform_device里面描述的设备信息放到设备树中。
也就说不需要编写platform_device这部分代码了,内核启动的时候会根据设备树节
点自动创建platform_device,并完成注册过程。
8.7.1platform设备树匹配设备树节点填写
linux-5.10.61/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts
myplatform{
compatible = "hqyj,myplatform";
reg = <0x12345678 49>;
interrupt-parent = <&gpiof>;
interrupts = <9 0>;
};
8.7.2platform设备树匹配驱动实例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
int irqno;
struct resource *p;
int pdrv_probe(struct platform_device *pdev){//获取device的设备信息
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
if(NULL==(p=platform_get_resource(pdev,IORESOURCE_MEM,0))){
printk("platform_get_resource error\n");
return EINVAL;
}
if(0>(irqno=platform_get_irq(pdev,0))){
printk("platform_get_irq error\n");
return irqno;
}
printk("%#x\n",p->start);
printk("%d\n",irqno);
return 0;
}
int pdrv_remove(struct platform_device *pdev){
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
const struct of_device_id oftable[]={
{.compatible="nyz,myplatform"},
{},//防止match在匹配的过程中越界访问
};
//1.分配对象并初始化
struct platform_driver pdrv={
.probe=pdrv_probe,
.remove=pdrv_remove,
.driver={.name="hi hi hi",
.of_match_table=oftable,},
};
module_platform_driver(pdrv);
MODULE_LICENSE("GPL");
8.8platform总线驱动练习
1.使用驱动代码实现如下要求
a.应用程序通过阻塞的IO模型来读取status的值
b.status是内核驱动中的一个变量(status代表LED1的状态)
c.status的值随着按键按下而改变(按键中断)
例如status=0 按下按键status=1 ,再次按下按键status=0
d.在按下按键的时候需要同时将LED1的状态取反
e.驱动中需要编写字符设备驱动
f.驱动中需要自动创建设备节点
g.这个驱动需要的所有设备信息放在设备树的同一个节点中
h.要求使用platform实现上述驱动
test.c
#include <stdio.h>
#include <head.h>
int main(int argc, char const *argv[])
{
int fd,status;
if(-1==(fd=open("/dev/pdrv",O_RDWR))){
ERRLOG("open error\n");
}
while(1){
if(-1==(read(fd,&status,4))){
ERRLOG("read error\n");
}
printf("status=%d\n",status);
}
close(fd);
return 0;
}
pdrv.c
#include <linux/init.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/timer.h>
#include <linux/fs.h>
#include <linux/of.h>
// test{
// compatible="nyz,test";
// interrupt-parent=<&gpiof>;
// interrupts=<9 0>;
// led1 = <&gpioe 10 0>;
// };
#define CNAME "pdrv"
int irqno,keygpio,ledgpio;
int status=0;
int major=0;
int condition=0;
struct timer_list mytimer;
struct class *cls;
struct device *dev;
wait_queue_head_t wq_head;
//按下按键->消抖,status=1->亮灯
void timer_handle(struct timer_list *tim){
if(!gpio_get_value(keygpio)){
status=!status;
gpio_set_value(ledgpio,status);
condition=1;
}
}
irqreturn_t key_led_irq_handle(int irq,void *dev){
//消抖
mod_timer(&mytimer, jiffies + 1);
return IRQ_HANDLED;
}
int pdrv_open(struct inode *inode, struct file *filp){
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
ssize_t pdrv_read(struct file *filp, char __user *ubuf, size_t size, loff_t *offs){
int ret;
//1.判断是否是非阻塞
if(filp->f_flags &O_NONBLOCK){
return EINVAL;
}else{
//2.放入等待队列
ret = wait_event_interruptible(wq_head, condition);
if (ret) {
printk("wait_event_interruptible by signal...\n");
return -ERESTARTSYS;
}
//3..将status拷贝到用户空间
if (size > sizeof(status)){
size = sizeof(status);
}
ret = copy_to_user(ubuf, &status, size);
if (ret) {
printk("copy_to_user failed\n");
return -EIO;
}
}
condition=0;
return 0;
}
int pdrv_close(struct inode *inode, struct file *filp){
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
struct file_operations fops = {
.open = pdrv_open,
.read = pdrv_read,
.release = pdrv_close,
};
//1.分配platform_driver并初始化
int pdrv_probe(struct platform_device *pdev){
int ret;
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
//驱动函数在probe里
//2.初始化定时器
mytimer.expires = jiffies + 1; // 10ms
timer_setup(&mytimer, timer_handle, 0);
add_timer(&mytimer);
//3.获取中断号
if(0>(irqno=platform_get_irq(pdev,0))){
printk("platform_get_irq error\n");
ret=irqno;
goto ERR1;
}
//4.获取gpio号 需要node pdev->dev.of_node
if(0>(ledgpio=of_get_named_gpio(pdev->dev.of_node,"leds",0))){
printk("of_get_named_gpio error\n");
ret=ledgpio;
goto ERR1;
}
if(0>(keygpio=of_get_named_gpio(pdev->dev.of_node,"keys",0))){
printk("of_get_named_gpio error\n");
ret=keygpio;
goto ERR1;
}
//5.申请gpio号
ret=gpio_request(ledgpio,NULL);
if(ret){
printk("gpio_request error\n");
goto ERR1;
}
//6.设置gpio输出
ret=gpio_direction_output(ledgpio,status);
if(ret){
printk("gpio_direction_output error\n");
goto ERR2;
}
//7.注册中断
if(0!=(ret=request_irq(irqno,key_led_irq_handle,IRQF_TRIGGER_FALLING,"test",NULL))){
printk("request_irq error\n");
goto ERR2;
}
//8.注册字符设备驱动
if(0>(major=register_chrdev(0,CNAME,&fops))){
printk("register_chrdev error\n");
goto ERR3;
}
//9.创建节点 cls dev
cls=class_create(THIS_MODULE,CNAME);
if(IS_ERR(cls)){
printk("class_create error\n");
ret = PTR_ERR(cls);
goto ERR4;
}
dev = device_create(cls, NULL, MKDEV(major, 0), NULL, CNAME);
if (IS_ERR(dev)) {
printk("device_create failed!\n");
ret = PTR_ERR(dev);
goto ERR5;
}
//10.初始化等待队列头
init_waitqueue_head(&wq_head);
return 0;
ERR5:
class_destroy(cls);
ERR4:
unregister_chrdev(major,CNAME);
ERR3:
free_irq(irqno,NULL);
ERR2:
gpio_free(ledgpio);
ERR1:
return ret;
}
int pdrv_remove(struct platform_device *pdev){
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
device_destroy(cls,MKDEV(major,0));
class_destroy(cls);
unregister_chrdev(major,CNAME);
free_irq(irqno,NULL);
gpio_free(ledgpio);
return 0;
}
//根据设备树获取
const struct of_device_id oftable[]={
{ .compatible = "nyz,test" },
{},
};
struct platform_driver pdrv={
.probe=pdrv_probe,
.remove=pdrv_remove,
.driver={
.name="hi",
.of_match_table=oftable,
},
};
module_platform_driver(pdrv);
MODULE_LICENSE("GPL");
9.i2c总线协议回顾
9.1i2c有几根线
scl:时钟线
sda:数据线
9.1.2i2c有几种信号
起始信号(start):当scl为高电平的时候,sda从高到低跳变 (主机发出)
停止信号(stop):当scl为高电平的时候,sda从低到高跳变 (主机发出)
应答信号(Ack):在第九个时钟周期的时候,sda管脚是低电平(从机可以给主机应答,主机也可以给从机应答)
非应答信号(No Ack):在第九个时钟周期的时候,sda管脚是高电平(一般是主机不给从机应答)
9.1.3i2c的读写协议
-
写协议
start+(7bit从机地址+1bit(写0))+ack+(8bit/16bit寄存器地址)+ack+(8bit/16bit数据)+ack+stop
-
读协议
start+(7bit从机地址+1bit(写0))+ack+(8bit/16bit寄存器地址)+ack+
start+(7bit从机地址+1bit(读1))+ack+(8bit/16bit从机给主机回复数据)+No Ack+stop
9.1.4i2c的7位/8位/10位从机地址寻址
7-bit 8-bit and 10-bit I2C Slave Addressing - Total Phase
9.1.5i2c总线的特点
i2c总线特点:i2c是同步的,半双工的,串行的,具备应答机制的总线协议
9.1.6i2c总线速率问题
100KHz 400KHz 1MHz(其他平台3.4MHz)
9.2i2c总线驱动
9.2.1i2c总线驱动的框架结构
注:在编写i2c设备驱动前需要先将i2c的核心层和总线驱动层选配到内核中,否则设备驱动无法工作。
控制器驱动配置到内核中:
│ -> Device Drivers
│ -> I2C support
│ -> I2C Hardware Bus support
│ <*> STMicroelectronics STM32F7 I2C support
核心层选配到内核中:
│ -> Device Drivers
│ I2C support
| -*- I2C support
9.2.2i2c设备驱动层API
1.分配并初始化对象
struct i2c_driver {
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
//匹配成功执行的函数
int (*remove)(struct i2c_client *client);
//分离的时候执行的函数
struct device_driver driver;
//父类
const struct i2c_device_id *id_table;
//1.idtable匹配方式
};
struct device_driver {
const char *name; //i2c中name不用于匹配,但是不必须填充
const struct of_device_id *of_match_table; //2.设备树匹配
}
2.注册
#define i2c_add_driver(driver) \
i2c_register_driver(THIS_MODULE, driver)
3.注销
void i2c_del_driver(struct i2c_driver *driver);
4.一键注册,注销的宏
module_i2c_driver(变量名);
9.2.3i2c设备驱动代码实例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
// probe函数
static int si7006_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
// remove函数
static int si7006_remove(struct i2c_client *client)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
// oftable
static const struct of_device_id oftable[] = {
{.compatible = "hqyj,si7006"},
{},
};
struct i2c_driver si7006 = {
.probe = si7006_probe,
.remove = si7006_remove,
.driver = {
.name = "si7006",
.of_match_table = oftable,
},
};
module_i2c_driver(si7006);
MODULE_LICENSE("GPL");
安装成功,节点可以识别到
9.2.4si7006的设备树编写
9.2.4.1画出硬件连接图
9.2.4.2找出控制器的设备树
arch/arm/boot/dts/stm32mp151.dtsi
i2c1: i2c@40012000 {
compatible = "st,stm32mp15-i2c";
reg = <0x40012000 0x400>;
interrupt-names = "event", "error";
interrupts-extended = <&exti 21 IRQ_TYPE_LEVEL_HIGH>,
<&intc GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rcc I2C1_K>;
resets = <&rcc I2C1_R>;
#address-cells = <1>;
#size-cells = <0>;
dmas = <&dmamux1 33 0x400 0x80000001>,
<&dmamux1 34 0x400 0x80000001>;
dma-names = "rx", "tx";
power-domains = <&pd_core>;
st,syscfg-fmp = <&syscfg 0x4 0x1>;
wakeup-source;
i2c-analog-filter;
status = "disabled";
};
9.2.4.3参考内核帮助文档编写自己的设备树
/home/linux/linux-5.10.61/Documentation/devicetree/bindings/i2c
参考st,stm32-i2c.yaml文件
参考i2c-st.txt
pinctrl不会写可以参考pinctrl的文件 在上一级目录里有
&i2c1{
pinctrl-names = "default","sleep";
pinctrl-0 = <&i2c1_pins_b>; //工作状态管脚设置
pinctrl-1 = <&i2c1_sleep_pins_b>; //休眠状态管脚设置
clock-frequency = <400000>; //控制器的频率
i2c-scl-rising-time-ns = <185>;
i2c-scl-falling-time-ns = <20>;
status = "okay"; //将控制器使能
si7006@40{ //si7006节点
compatible = "hqyj,si7006";
reg = <0x40>;
};
};
检查节点,忘记加引用了
9.2.5i2c驱动数据封装和发送
9.2.5.1i2c数据封装和发送需要用到的结构体
当i2c驱动进入到probe函数的时候会收到内核传递的i2c_client结构体,这个结构体就是
设备驱动和总线驱动匹配成功时候内核创建的,这个i2c_client结构体是用来携带信息的
结构体(从机地址,标志位,控制器驱动对象等等)。
struct i2c_client {
unsigned short flags; //i2c的标志位
//I2C_CLIENT_TEN 10位从机地址
//I2C_CLIENT_SLAVE 标识它是从机
//I2C_CLIENT_SCCB 摄像头特有的总线协议SCCB
unsigned short addr; //从机地址
char name[I2C_NAME_SIZE]; //名字
struct i2c_adapter *adapter; //控制器驱动对象
struct device dev; //dev.of_node 节点结构体指针
int irq; //软中断号
};
设备驱动和总线驱动数据交互的时候使用的就是i2c_msg结构体
struct i2c_msg {
__u16 addr; //从机地址 client->addr
__u16 flags; // 1读 0写
__u16 len; //消息的长度
__u8 *buf; //指向消息的首地址
};
9.2.5.2i2c消息结构体的封装
有多少个起始信号就有多少个消息,消息的长度是以字节来表示的
-
写消息结构体封装
start+(7bit从机地址+1bit(写0))+ack+(8bit/16bit寄存器地址)+ack+(8bit/16bit数据)+ack+stop
(7bit从机地址+1bit(写0))+(8bit寄存器地址)+(8bit数据)
char w_buf[] = {reg,data}; struct i2c_msg w_msg = { .addr = client->addr, .flags = 0, .len = 2, .buf = w_buf, };
-
读消息结构体封装
start+(7bit从机地址+1bit(写0))+ack+(8bit/16bit寄存器地址)+ack+
start+(7bit从机地址+1bit(读1))+ack+(8bit/16bit从机给主机回复数据)+No Ack+stop
(7bit从机地址+1bit(写0))+(8bit寄存器地址)+
(7bit从机地址+1bit(读1))+(8bit从机给主机回复数据)
char r_buf[] = {reg}; char data; struct i2c_msg r_msg[] = { [0] = { .addr = client->addr, .flags=0, .len = 1, .buf = r_buf, }, [1] = { .addr = client->addr, .flags = 1, .len = 1, .buf = &data, }, };
9.2.5.3i2c消息结构体的发送
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
功能:消息结构体的发送
参数:
@adap:总线驱动对象client->adapter(发给谁)
@msgs:消息结构体的首地址(发什么)
@num:消息的个数(发多少)
返回值:成功返回num,否则就是失败
9.2.6si7006驱动读取温湿度实例
si7006.h
#ifndef __SI7006_H__
#define __SI7006_H__
10.spi总线驱动
10.1spi总线协议回顾
-
spi总线特点
SPI 是串行外设接口(Serial Peripheral Interface)的缩写。它
是 Motorola 公司推出的一种同步串行接口技术,是一种高
速的,全双工,同步的通信总线。
SPI优点:
支持全双工通信,通信简单,数据传输速率快
1):高速、同步、全双工、非差分、总线式
2):主从机通信模式
缺点:
没有指定的流控制,没有应答机制确认是否接收到数据,
所以跟IIC总线协议比较在数据的可靠性上有一定的缺陷。
-
spi管脚及模式
可以一主机多从机,具体和那个从机通讯通过cs片选决定。
MISO :主机输入,从机输出
MOSI :主机输出,从机输入
SCK :时钟线(只能主机控制)
CS :片选线
数据传输的四种方式:
CPOL(时钟极性) : 0:时钟起始位低电平,1:时钟起始为高电平
CPHA(时钟相位) :0:第一个时钟周期采样,1:第二个时钟周期采样
-
spi协议解析
CPOL=0,CPHA=0:此时空闲态时,SCLK处于低电平,数据
采样是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,
所以数据采样是在上升沿,数据发送是在下降沿。
CPOL=0,CPHA=1:此时空闲态时,SCLK处于低电平,数据
发送是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,
所以数据采样是在下降沿,数据发送是在上升沿。
CPOL=1,CPHA=0:此时空闲态时,SCLK处于高电平,数据
采集是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,
所以数据采集是在下降沿,数据发送是在上升沿。
CPOL=1,CPHA=1:此时空闲态时,SCLK处于高电平,数据
发送是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,
所以数据采集是在上升沿,数据发送是在下降沿。
10.2spi驱动框架结构
将stm32mp157a中spi控制器设备树和核心层选配到内核中
在linux_5.10.61/drivers/spi下 vi Makefile中找对应厂商
然后在顶层目录下make menuconfig搜
找到对应路径选中
spi控制器驱动配置: Device Drivers ---> [*] SPI support ---> <*> STMicroelectronics STM32 SPI controller spi核心层配置: Device Drivers ---> [*] SPI support ---> 重新编译内核 make uImage LOADADDR=0xc2000000 将编译好的内核拷贝到tftpboot目录下 cp arch/arm/boot/uImage ~/tftpboot/
10.3spi设备驱动的API
1.分配并初始化对象
struct spi_driver {
int (*probe)(struct spi_device *spi);
//匹配成功执行probe
int (*remove)(struct spi_device *spi);
//分离执行remove
struct device_driver driver;
//父类
};
struct device_driver{
const char *name; //名字
const struct of_device_id *of_match_table; //设备树匹配
};
2.注册
#define spi_register_driver(driver) \
__spi_register_driver(THIS_MODULE, driver)
3.注销
void spi_unregister_driver(struct spi_driver *sdrv)
4.一键注册,注销
module_spi_driver(spi_driver变量名);
10.4spi总线驱动代码实例1
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
int m74hc595_probe(struct spi_device* spi)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
int m74hc595_remove(struct spi_device* spi)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
const struct of_device_id oftable[] = {
{ .compatible = "hqyj,m74hc595", },
{},
};
struct spi_driver m74hc595 = {
.probe = m74hc595_probe,
.remove = m74hc595_remove,
.driver = {
.name = "m74ch595",
.of_match_table = oftable,
},
};
module_spi_driver(m74hc595);
MODULE_LICENSE("GPL");
可以运行,具体能不能用还要添加设备树节点之后才能验证
10.5编写spi相关的设备树
10.5.1画出硬件连接关系图
10.5.2找出控制器的设备树
arch/arm/boot/dts/stm32mp151.dtsi 文件内找spi4
spi4: spi@44005000 {
#address-cells = <1>; //修饰子节点reg地址个数
#size-cells = <0>; //修饰子节点reg长度个数
compatible = "st,stm32h7-spi";
reg = <0x44005000 0x400>;
interrupts = <GIC_SPI 84 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rcc SPI4_K>;
resets = <&rcc SPI4_R>;
dmas = <&dmamux1 83 0x400 0x01>,
<&dmamux1 84 0x400 0x01>;
dma-names = "rx", "tx";
power-domains = <&pd_core>;
status = "disabled"; //控制器默认没有使能
};
10.5.3参考内核帮助文档编写自己的设备树
Documentation/devicetree/bindings/spi 里找对应厂商的文档,看里面的例子
发现需要增加片选和自己的spi设备子节点,还需将status改成okay,
查看控制器是否使能 /proc/device-tree/soc/spi 下查看
当前写的节点
除了gpioe11还有三个引脚没写,参考pinctrl的文件stm32mp15-pinctrl.dtsi
在里面找spi4
发现两个合适的节点,完善自己的节点
编译下载运行
设备树可以识别到
&spi4{
pinctrl-names = "default","sleep";
pinctrl-0 = <&spi4_pins_b>; //工作状态管脚设置
pinctrl-1 = <&spi4_sleep_pins_b>; //休眠状态管脚设置
cs-gpios = <&gpioe 11 0>; //片选管脚
status = "okay"; //控制器使能
m74hc595@0{
compatible = "hqyj,m74hc595";
reg = <0>; //片选下标编号
spi-max-frequency = <10000000>; //工作频率
//spi-cpol; //如果写了就代表cpol=1,否则cpol=0
//spi-cpha; //如果写了就代表cpha=1,否则cpha=0
};
};
10.6spi数据收发接口
int spi_write(struct spi_device *spi, const void *buf, size_t len) //发数据
int spi_read(struct spi_device *spi, void *buf, size_t len) //接收数据
int spi_write_then_read(struct spi_device *spi, //同时收发
const void *txbuf, unsigned n_tx,
void *rxbuf, unsigned n_rx);
10.7使用spi让4个数码管显示5
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
int m74hc595_probe(struct spi_device* spi)
{
char w_buf[] = {0xf,0x6d}; //4个数码管显示5
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
spi_write(spi,w_buf,sizeof(w_buf));
return 0;
}
int m74hc595_remove(struct spi_device* spi)
{
char w_buf[] = {0xf,0}; //4个数码管不显示
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
spi_write(spi,w_buf,sizeof(w_buf));
return 0;
}
const struct of_device_id oftable[] = {
{ .compatible = "hqyj,m74hc595", },
{},
};
struct spi_driver m74hc595 = {
.probe = m74hc595_probe,
.remove = m74hc595_remove,
.driver = {
.name = "m74ch595",
.of_match_table = oftable,
},
};
module_spi_driver(m74hc595);
MODULE_LICENSE("GPL");
10.8通过应用程序控制数码管显示数据
m74hc595.h
#ifndef __M74HC595_H__
#define __M74HC595_H__
#define SEG_WHICH _IOW('k',0,int)
#define SEG_DAT _IOW('k',1,int)
#endif
m74hc595.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/fs.h>
#include "m74hc595.h"
/*
&spi4 {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&spi4_pins_b>;
pinctrl-1 = <&spi4_sleep_pins_b>;
cs-gpios = <&gpioe 11 GPIO_ACTIVE_HIGH>;
status = "okay";
m74hc595@0{
compatible = "hqyj,m74hc595";
reg = <0>;
spi-max-frequency = <40000000>;
};
};
*/
#define NAME "m74hc595"
int major = 0;
struct class *cls;
struct device *dev;
struct spi_device *gspi;
u8 code[] = {
0x3f, //0
0x06, //1
0x5b, //2
0x4f, //3
0x66, //4
0x6d, //5
0x7d, //6
0x07, //7
0x7f, //8
0x6f, //9
0x77, //A
0x7c, //b
0x39, //c
0x5e, //d
0x7b, //e
0x71, //f
};
u8 which[] = {
0x1, //sg0
0x2, //sg1
0x4, //sg2
0x8, //sg3
};
int m74hc595_open(struct inode *inode, struct file *file)
{
printk("%s:%d\n",__func__,__LINE__);
return 0;
}
long m74hc595_ioctl(struct file *file, unsigned int cmd, unsigned long args)
{
switch(cmd){
case SEG_WHICH:
spi_write(gspi,&which[args],1);
break;
case SEG_DAT:
spi_write(gspi,&code[args],1);
break;
default: printk("ioctl error\n");break;
}
return 0;
}
int m74hc595_close(struct inode *inode, struct file *file)
{
printk("%s:%d\n",__func__,__LINE__);
return 0;
}
struct file_operations fops = {
.open = m74hc595_open,
.unlocked_ioctl = m74hc595_ioctl,
.release = m74hc595_close,
};
int m74hc595_probe(struct spi_device *spi)
{
u8 buf[2] = {0xf,0x0};
printk("%s:%d\n",__func__,__LINE__);
gspi = spi;
spi_write(gspi,buf,ARRAY_SIZE(buf));
major = register_chrdev(0,NAME,&fops);
if(major < 0){
printk("register chrdev error\n");
return major;
}
cls = class_create(THIS_MODULE,NAME);
if(IS_ERR(cls)){
printk("class create error\n");
return PTR_ERR(cls);
}
dev = device_create(cls,NULL,MKDEV(major,0),NULL,NAME);
if(IS_ERR(dev)){
printk("device create error\n");
return PTR_ERR(dev);
}
return 0;
}
int m74hc595_remove(struct spi_device *spi)
{
printk("%s:%d\n",__func__,__LINE__);
device_destroy(cls,MKDEV(major,0));
class_destroy(cls);
unregister_chrdev(major,NAME);
return 0;
}
const struct of_device_id of_match[] = {
{.compatible = "hqyj,m74hc595",},
{},
};
struct spi_driver m74hc595 = {
.probe = m74hc595_probe,
.remove = m74hc595_remove,
.driver = {
.name = "hello_m74hc595",
.of_match_table = of_match,
},
};
module_spi_driver(m74hc595);
MODULE_LICENSE("GPL");
test.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "m74hc595.h"
int main(int argc, const char *argv[])
{
int which=0;
int data=0;
int fd;
fd = open("/dev/m74hc595",O_RDWR);
if(fd < 0){
perror("open error");
return -1;
}
while(1){
ioctl(fd,SEG_WHICH,which++);
ioctl(fd,SEG_DAT,data++);
if(which >= 4)which=0;
if(data >= 16)data = 0;
sleep(1);
}
close(fd);
return 0;
}
11.Linux内核中内存分配
11.1地址的概念
物理地址:在datasheet中能够查到的地址称之为物理地址,实际设备的操作地址;
虚拟地址、线性地址:在操作系统程序员能够操作的地址称之为虚拟地址;
逻辑地址:将程序进行反汇编之后,其中能够看到的地址称之为逻辑地址;
11.2内存管理
段式管理:x86架构,分段管理
页式管理:arm架构,页式管理
11.3内存映射关系
11.4linux内核空间内存分配的函数
void *kmalloc(size_t s, gfp_t gfp)
功能:分配对应的虚拟内存
参数:size:分配内存区的大小
flags:内存分配标志
GFP_KERNEL:内核可能被休眠,用于进程上下文中
GFP_ATOMIC:处理紧急的事务,用在中断上下文
返回值:对应虚拟地址
特点:最大128k , 分配虚拟地址,其虚拟地址空间连续,
物理地址空间也是连续,分配的内存必须是2的次幂的形式
类似函数:kzalloc = kmalloc+memset(,0,):分配虚拟内存区并清零
void kfree(const void *x)
功能:释放对应的虚拟内存
参数:x:虚拟内存的起始地址
返回值:无
void *vmalloc(unsigned long size)
功能:分配对应的虚拟内存
参数:size:分配内存区的大小
返回值:对应虚拟地址
特点:分配虚拟地址,其虚拟地址空间连续,
但是物理地址空间不一定连续
void vfree(const void *addr)
功能:释放对应的虚拟内存
参数:addr:虚拟内存区的首地址
返回值:无
unsigned long __get_free_page(gfp_t gfp)
功能:分配一个页的内存 4K
void free_page(unsigned long addr)
释放一个页的内存
unsigned long __get_free_pages(gfp_t gfp_mask, get_order(57600))
功能:分配多个页的内存
57600-->2^n :第二个参数填写的是n
n = get_order(57600)
void free_pages(unsigned long addr, unsigned long order)
释放多个页的内存
11.5实例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
char *buf;
static int __init mycdev_init(void)
{
buf = kzalloc(128,GFP_KERNEL);
if(buf == NULL){
printk("kzalloc memory error\n");
return -ENOMEM;
}
memcpy(buf,"i am test kzalloc func....\n",
strlen("i am test kzalloc func....\n"));
printk("buf = %s\n",buf);
return 0;
}
static void __exit mycdev_exit(void)
{
kfree(buf);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");
12.块设备驱动
块设备驱动是用来操作硬盘类的存储设备的
12.1什么是块设备驱动
- 块设备驱动的概念:系统中能够随机访问固定大小(1block 512byte)数据片的设备被称之为块设备。块设备文件一般都是以安装文件系统的方式使用,这也是块设备通常的访问方式。块设备的方式访问方式是随机的。
- 块设备中最小的可寻址单位是扇区,扇区大小一般是2的整数倍。最常见的大小是512字节。块是文件系统的一种抽象,只能基于块来访问文件系统。物理磁盘寻址是按照扇区的级别进行的,内核访问的所有磁盘操作又都是按照块进行的。扇区是设备的最小可寻址单位,所以块不能比扇区还小,只能数倍与扇区大小。
- 内核对块大小的要求是:必须是扇区大小的整数倍,并且小于页面的大小,所以块的大小通常是512字节、1K或者4K。
1.2块设备驱动和字符设备驱动的对比
- 块设备接口相对复杂,不如字符设备明晰易用
- 块设备驱动程序对整个系统的性能影响较大,速度和效率是设计块设备驱动程要重点考虑的问题
- 系统中使用缓冲区与访问请求的优化管理(合并与重新排序)来提高系统性能
1.3块设备驱动的相关知识简介
磁头:一个磁盘有多少个面就有多少个磁头
磁道:在一个磁头上可以有很多环,这些环就叫做磁道
扇区:磁道上访问数据的最小的单位就是扇区,一个扇区的大小就是512字节
1block = 512字节 1024字节 2048字节 4096字节
1扇区 = 512字节
块设备的能存储的数据=磁头*磁道*扇区*512
1.4块设备驱动的框架图
- 虚拟文件系统(VFS):隐藏了各种硬件的具体细节,为用户操作不同的硬件提供了一个统一的接口。其基于不同的文件系统格式,比如EXT,FAT等。用户程序对设备的操作都通过VFS来完成,在VFS上面就是诸如open、close、write和read的函数API。
- Disk Cache:硬盘的高速缓存,用户缓存最近访问的文件数据,如果能在高速缓存中找到,就不必去访问硬盘,毕竟硬盘的访问速度慢很多。
- 映射层(mapping layer):这一层主要用于确定文件系统的block size,然后计算所请求的数据包含多少个block。同时调用具体文件系统函数来访问文件的inode,确定所请求的数据在磁盘上面的逻辑地址。
- Generic Block Layer:通用块层,Linux内核把块设备看做是由若干个扇区组成的数据空间,上层的读写请求在通用块层被构造成一个或多个bio结构
- I/O Scheduler Layer :I/O调度层,负责将通用块层的块I/O操作进行(电梯调度算法)调度、插入、暂存、排序、合并、分发等操作,对磁盘的操作更为高效。负责将通用块层的块I/O操作进行
- 块设备驱动层:在块系统架构的最底层,由块设备驱动根据排序好的请求,对硬件进行数据访问。
user:
open read write close
-------------------(io请求)-----------------------------------
kernel |中间层: (block_device)
| 将用户的io请求转化成BIO(block,input ,output),
| 在物理内存上连续的bio会被合成request,这个request
| 会被放到内核的一个队列上。
|---------------------------------------------------------
|driver:gendisk
| 1.分配对象
| 2.对象初始化
| 3.初始化一个队列 head----request(read)----request(write)---...
| //4.硬盘设备的初始化
| 5.注册、注销
------------------------------------------------------------------
haredware : 分配的内存(模拟真实的设备)(1M)
1.5块设备驱动的API
1.gendisk的结构体对象
struct gendisk {
int major; //块设备的主设备号
int first_minor; //起始的次设备号
int minors; //设备的个数,分区的个数
char disk_name[DISK_NAME_LEN]; //磁盘的名字
struct disk_part_tbl *part_tbl;
//磁盘的分区表的首地址
struct hd_struct part0;
//part0分区的描述
const struct block_device_operations *fops;
//块设备的操作方法结构体
struct request_queue *queue;
//队列(重要)
void *private_data;
//私有数据
};
分区的结构体
struct hd_struct {
sector_t start_sect; //起始的扇区号
sector_t nr_sects; //扇区的个数
int partno; //分区号
};
//块设备的操作方法结构体
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
int (*release) (struct gendisk *, fmode_t);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*getgeo)(struct block_device *, struct hd_geometry *);
//设置磁盘的磁头,磁道,扇区的个数的。hd_geometry
}
2.结构体对象的初始化
struct gendisk *mydisk;
struct gendisk *alloc_disk(int minors)
//void put_disk(struct gendisk *disk)
//归还引用计数
功能:分配gendisk的内存,然后完成必要的初始化
参数:
@minors:分区的个数
返回值:成功返回分配到的内存的首地址,失败返回NULL
int register_blkdev(unsigned int major, const char *name)
//void unregister_blkdev(unsigned int major, const char *name)
功能:申请设备设备驱动的主设备号
参数:
@major : 0:自动申请
>0 :静态指定
@name :名字 cat /proc/devices
返回值:
major=0 ;成功返回主设备号,失败返回错误码
major>0 :成功返回0 ,失败返回错误码
void set_capacity(struct gendisk *disk, sector_t size)
功能:设置磁盘的容量
struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,
const struct blk_mq_ops *ops,unsigned int queue_depth,unsigned int set_flags)
//void blk_cleanup_queue(struct request_queue *q)
功能:用于在给定队列深度的情况下使用mq ops设置队列的助手,以及通过mq ops标志传递的助手
参数:
@被初始化的tag对象,tag被上层使用,里面包含硬件队列的个数,队列的操作方法结构体,标志位等
@放入到tag中的操作方法结构体
@tag中指定支持的队列深度
@将tag中队列的处理标志位,例如BLK_MQ_F_SHOULD_MERGE, BLK_MQ_F_BLOCKING等
返回值:成功返回队列指针,失败返回错误码指针
3.注册、注销
void add_disk(struct gendisk *disk)
//注册
void del_gendisk(struct gendisk *disk)
//注销
1.6块设备驱动实例1
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/init.h>
#include <linux/module.h>
#define BLKSIZE (1 * 1024 * 1024) // 1M
#define BLKNAME "mydisk"
struct gendisk* mydisk;
int major;
char *dev_addr; //1M内存首地址,当成磁盘使用的
int mydisk_open(struct block_device* dev, fmode_t mode)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
int mydisk_getgeo(struct block_device* dev, struct hd_geometry* geo)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
geo->heads = 4; //磁头
geo->cylinders = 16; //磁道
geo->sectors = BLKSIZE / geo->heads / geo->cylinders / 512; //扇区
return 0;
}
void mydisk_release(struct gendisk* disk, fmode_t mode)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
const struct block_device_operations fops = {
.open = mydisk_open,
.release = mydisk_release,
.getgeo = mydisk_getgeo,
};
static int __init mydisk_init(void)
{
// 1.分配对象
mydisk = alloc_disk(4);
if (mydisk == NULL) {
printk("alloc_disk error\n");
return -ENOMEM;
}
// 2.对象初始化
// 2.1申请主设备号
major = register_blkdev(0, BLKNAME);
if (major < 0) {
printk("register_blkdev error\n");
return -EBUSY;
}
// 2.2设置磁盘容量
set_capacity(mydisk, BLKSIZE >> 9);
// 2.3队列的申请
// 2.4mydisk成员赋值
mydisk->major = major;
mydisk->first_minor = 0;
strcpy(mydisk->disk_name, BLKNAME);
mydisk->fops = &fops;
// mydisk->queue = ?
// 3.申请1M内存当成磁盘使用
dev_addr = vmalloc(BLKSIZE);
if (dev_addr == NULL) {
printk("vmalloc error\n");
return -ENOMEM;
}
// 4.注册
add_disk(mydisk);
return 0;
}
static void __exit mydisk_exit(void)
{
del_gendisk(mydisk);
vfree(dev_addr);
unregister_blkdev(major, BLKNAME);
put_disk(mydisk);
}
module_init(mydisk_init);
module_exit(mydisk_exit);
MODULE_LICENSE("GPL");
1.7队列处理相关的结构体、关系、相关函数
struct request_queue
{
/*双向链表数据结构,将所有加入到队列的IO请求组建成一个双向链表*/
struct list_head queue_head;
struct list_head requeue_list; //request队列
spinlock_t requeue_lock; //队列自旋锁
unsigned long nr_requests; /* 最大的请求数量 */
unsigned long queue_flags;/*当前请求队列的状QUEUE_FLAG_STOPPED*/
…
};
struct request
{
struct list_head queuelist;/* 请求对象中的链表元素*/
struct request_queue *q; /* 指向存放当前请求的请求队列*/
unsigned int __data_len; /* 当前请求要求数据传输的总的数据量 */
sector_t __sector; /* 当前请求要求数据传输的块设备的起始扇区 */
struct bio *bio; /* bio对象所携带的信息转存至请求对象中*/
struct bio *biotail; /* bio链表*/
…
};
通常一个request请求可以包含多个bio,一个bio对应一个I/O请求
struct bio {
struct bio *bi_next; /* 指向当前bio的下一个对象*/
unsigned long bi_flags; /* 状态、命令等 */
unsigned long bi_rw; /* 表示READ/WRITE*/
struct block_device *bi_bdev; /* 与请求相关联的块设备对象指针*/
unsigned short bi_vcnt; /* bi_io_vec数组中元素个数 */
unsigned short bi_idx; /* 当前处理的bi_io_vec数组元素索引 */
unsigned int bi_size; /* 本次传输需要传输的数据总量,byte(扇区大小整数倍) */
struct bio_vec *bi_io_vec;/* 指向一个IO向量的数组,数组中的内各元素对应一个物理页的page对象 */
};
struct bio_vec {
struct page *bv_page; //指向用于数据传输的页面所对应的struct page对象
unsigned int bv_len; //表示当前要传输的数据大小
unsigned int bv_offset;//表示数据在页面内的偏移量
};
reque_queue和request及bio的关系
1.8虚拟块设备驱动
#include <linux/init.h>
#include <linux/module.h>
#include <linux/blk-mq.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/vmalloc.h>
#define BLKNAME "mydisk"
//1.分配对象
struct gendisk *mydisk;
int major;
#define BLKSIZE 1*1024*1024 //
char* dev_addr; // 1M内存首地址,当成磁盘使用的
//2.6.1分配队列对象
struct request_queue *queue;
int mydisk_open(struct block_device *dev, fmode_t mode){
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
void mydisk_release(struct gendisk * disk, fmode_t mode){
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
int mydisk_getgeo(struct block_device *dev , struct hd_geometry * geo){
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
//2.5.2 磁头磁道扇区
geo->heads=4;
geo->cylinders=16;
geo->sectors=BLKSIZE/geo->heads/geo->cylinders;
return 0;
}
//2.5.1构造操作方法结构体
const struct block_device_operations fops={
.open=mydisk_open,
.release=mydisk_release,
.getgeo=mydisk_getgeo,
};
//2.6.2创建初始化对象 给上层留的接口
struct blk_mq_tag_set tags;
//2.6.4操作方法结构体访问操作队列的函数
//操作队列的函数 相当于probe release remove
blk_status_t mydisk_queue_rq(struct blk_mq_hw_ctx* ctx,
const struct blk_mq_queue_data* bd){
//在这个函数里要完成数据的读写入磁盘
struct request* rq=bd->rq;//找到request
struct bio_vec bvec;//找到bio_vec:request-->struct bio-->struct bio_vec
//ii.i需先定义迭代器
struct req_iterator iter;
//找磁盘的地址rq_for_each_segment
//i.告诉上层开始处理队列了
blk_mq_start_request(rq);
unsigned long pos = rq->__sector * 512;
//ii.获取到内存片段的线性地址
rq_for_each_segment(bvec,rq, iter){
//将磁盘地址转换为内存片段的线性地址
void* b_buf = page_address(bvec.bv_page) + bvec.bv_offset;
unsigned long len = bvec.bv_len;
// 2.2根据读写完成拷贝
if (rq_data_dir(rq)) { // 1 Write
// 目的:磁盘地址 dev_addr+pos
// 源: b_buf
// 大小:len
memcpy(dev_addr + pos, b_buf, len);
} else { // 0 Read
// 目的:b_buf
// 源: 磁盘地址dev_addr+pos
// 大小:len
memcpy(b_buf, dev_addr + pos, len);
}
pos += len;
}
//iii.告诉上层队列处理结束
blk_mq_end_request(rq, BLK_STS_OK);
return BLK_STS_OK;
}
//2.6.3填充操作方法结构体
const struct blk_mq_ops qops={
.queue_rq=mydisk_queue_rq,
};
static int __init mydisk_init(void)
{
//2.对象初始化
if(NULL==(mydisk=alloc_disk(4))){//部分初始化
printk("alloc_disk error\n");
return -ENOMEM;
}
//剩下的初始化需要自己完成
//2.1申请主设备号
if(0>(major=register_blkdev(0,BLKNAME))){
printk("register_blkdev error\n");
return -EBUSY;
}
mydisk->major = major;
//2.2起始次设备号
mydisk->first_minor=0;
//2.3磁盘名
strcpy(mydisk->disk_name,BLKNAME);
//2.4part0分区
set_capacity(mydisk,BLKSIZE/512);
//2.5操作方法结构体
mydisk->fops=&fops;
//2.6申请队列
queue=blk_mq_init_sq_queue(&tags,&qops,16,BLK_MQ_F_SHOULD_MERGE);
if (IS_ERR(queue)) {
printk("blk_mq_init_sq_queue error\n");
return -ENOMEM;
}
mydisk->queue = queue;
//4.申请1M内存当成磁盘使用
dev_addr = vmalloc(BLKSIZE);
if (dev_addr == NULL) {
printk("vmalloc error\n");
return -ENOMEM;
}
//5.注册
add_disk(mydisk);
return 0;
}
static void __exit mydisk_exit(void)
{
//5.注销
del_gendisk(mydisk);
vfree(dev_addr);
blk_cleanup_queue(queue);
unregister_blkdev(major, BLKNAME);
put_disk(mydisk);
}
module_init(mydisk_init);
module_exit(mydisk_exit);
MODULE_LICENSE("GPL");
1.9块设备驱动的测试方法
1.安装块设备驱动
sudo insmod mydisk.ko
2.查看设备节点
ls -l /dev/mydisk
brw-rw---- 1 root disk 252, 0 6月 22 14:14 /dev/mydisk
3.查看磁盘相关的命令
sudo fdisk -l
Disk /dev/mydisk:1 MiB,1048576 字节,2048 个扇区
单元:扇区 / 1 * 512 = 512 字节
扇区大小(逻辑/物理):512 字节 / 512 字节
I/O 大小(最小/最佳):512 字节 / 512 字节
4.分区
sudo fdisk /dev/mydisk
m 获取帮助
d 删除分区
p 打印分区表
n 添加新分区
w 将分区表写入磁盘并退出
q 退出而不保存更改
在dev下产生/dev/mydisk1节点
5.分区格式化
sudo mkfs.ext2 /dev/mydisk1
6.挂载硬盘到某个目录下
sudo mount -t ext2 /dev/mydisk1 ~/udisk
7.向磁盘中存放文件
sudo cp ~/work/day11/01mydisk/mydisk.c .
8.取消挂载
cd
sudo umount udisk
9.将块设备驱动的数据读取到ram.bin文件中
sudo cat /dev/mydisk1 > ram.bin
10.重新挂载查看
sudo mount -o loop ram.bin ~/udisk
去udisk目录下查看是否有刚才拷贝的文件,如果有就说明成功了
2.摄像头驱动硬件介绍
2.1ov5640简介
一、OV5640简介
OV5640是OV(OmniVision)公司推出的一款CMOS图像传感器,实际感光阵列为:2592 x 1944(即500w像素),该传感器内部集成了图像处理的电路,包括自动曝光控制(AEC)、自动白平
衡( AWB) 等。同时该传感器支持LED补光、 MIPI(移动产业处理器接口,多用于手机等)输出接口和DVP(数字视频并行,在设计HDMI显示时,就用的这个)输出接口选择、 ISP(图像信号处理)以及自动聚焦控制(VCM)等功能。支持输出RAW RGB, RGB565/555/444, CCIR656, YUV422/420, YCbCr422, 和图像格式压缩JPEG。
2.2OV5640内部构造图
可以看到,时序发生器和系统控制逻辑(timing generator and system control logic)控制着感光阵列(image array)、放大器(AMP)、AD转换(10bit)以及输出外部时序信号(PCLK和行场同步信号等)。感光阵列输出模拟信号,经过AMP增强信号强度,进入到AD转换器,转换成数字信号并经过ISP,进行相关图像处理,最终输出10位DVP数据流或者MIPI数据流。AMP和ISP等都是由控制寄存器进行控制,而配置寄存器的接口时序就是使用的SCCB。由于OV5640寄存器较多,OV5640寄存器的地址为16位,所以SCCB协议中的寄存器地址为16位。
2.3OV5640采集的图像分辨率
2.4OV5640接口简介
2.5摄像头图像输出接口
MIPI
MIPI是差分串口传输,速度快,抗干扰。主流手机模组现在都是用MIPI传输,传输时使用4对差分信号传输图像数据和一对差分时钟信号;最初是为了减少LCD屏和主控芯片之间连线的数量而设计的,后来发展到高速了,支持高分辨率的显示屏,现在基本上都是MIPI接口了。 MIPI的camera接口叫 CSI,MIPI的display接口叫DSI。
DVP
DVP是并口传输,速度较慢,传输的带宽低,使用需要PCLK\sensor输出时钟、MCLK(XCLK):外部时钟输入、VSYNC:场同步、HSYNC:行同步、D[0:11]:并口数据—可以是8/10/12bit数据位数大小。
2.6摄像头外设管脚工作原理
2.7SCCB总线
SCCB(SeriaI Camera ControlBus)是简化的I2C协议,SIO-l是串行时钟输入线,SIO-O是串行双向数据线,分别相当于I2C协议的SCL和SDA。SCCB的总线时序与I2C基本相同,它的响应信号ACK被称为一个传输单元的第9位,分为Don’t care和NA。Don’t care位由从机产生;NA位由主机产生,由于SCCB不支持多字节的读写,NA位必须为高电平。另外,SCCB没有重复起始的概念,因此在SCCB的读周期中,当主机发送完片内寄存器地址后,必须发送总线停止条件。不然在发送读命令时,从机将不能产生Don’t care响应信号。
3.摄像头V4L2驱动框架
3.1v4l2架构的功能(video for linux 2)
V4l2可以支持多种设备,它可以有以下几种接口:
1.视频采集接口(Video Capture Interface):V4L2最初设计用于支持视频
采集设备,例如高频头或摄像头。视频采集接口允许应用程序访问和控
制这些设备,以获取从摄像头或其他视频源采集到的视频数据。
2.视频输出接口(Video Output Interface):V4L2还可以驱动计算机外
围的视频图像设备,如能够输出电视信号格式的设备(例如电视机、显
示器)。通过视频输出接口,V4L2允许将计算机中生成的视频图像传输
到外部显示设备进行展示。
3.直接传输视频接口(Video Overlay Interface):该接口的主要功能是直
接将从视频采集设备采集到的信号输出到输出设备上,而不需要经过系统
的中央处理器(CPU)。这种传输方式可以提高视频播放的效率和质量,
并减轻CPU的负载。
4.视频间隔消隐信号接口(VBI Interface):VBI接口允许应用程序访问传输
中的视频间隔消隐(VBI)信号。VBI信号是一种特殊的视频信号,用于传
输附加的信息,例如字幕、广告和时间码。通过VBI接口,应用程序可以
提取和处理这些附加信息。
5.收音机接口(Radio Interface):该接口用于处理从AM或FM高频头设备接
收到的音频流。V4L2中的收音机接口允许计算机与收音机硬件进行交互,
接收和处理收音机信号,以实现收音
3.2v4l2驱动框架图
3.3V4L2驱动编写流程
1.分配对象
struct video_device *vfd;
vfd = video_device_alloc();
2.对象的初始化
vfd->name = "ov5640",
vfd->ops = &ov5640_fops, //v4l2核心类的操作方法结构体v4l2_file_operations
vfd->ioctl_ops = &ov5640_ioctl_ops, //v4l2的ioctl操作方法 v4l2_ioctl_ops
vfd->release = video_device_release,
snprintf(dev->v4l2_dev.name, sizeof(dev->v4l2_dev.name),
"%s-%03d", VIVI_MODULE_NAME, inst);
v4l2_device_register(NULL, &dev->v4l2_dev);
vfd->v4l2_dev = &dev->v4l2_dev;
3.注册
video_register_device(vfd, VFL_TYPE_GRABBER, video_nr);
/*
__video_register_device(vdev, type, nr, 1, vdev->fops->owner);
switch (type) { ===>指定本次分配的设备的类型
case VFL_TYPE_GRABBER:
name_base = "video";
break;
case VFL_TYPE_VBI:
name_base = "vbi";
break;
case VFL_TYPE_RADIO:
name_base = "radio";
break;
}
switch (type) {
case VFL_TYPE_GRABBER:
minor_offset = 0; ===>指定次设备号的范围
minor_cnt = 64;
break;
case VFL_TYPE_RADIO:
minor_offset = 64;
minor_cnt = 64;
break;
case VFL_TYPE_VBI:
minor_offset = 224;
minor_cnt = 32;
break;
default:
minor_offset = 128;
minor_cnt = 64;
break;
}
vdev->cdev = cdev_alloc(); ====>注册了字符设备驱动
vdev->cdev->ops = &cdev_fops; //这是字符设备驱动的操作方法结构体file_operations
vdev->cdev->owner = owner;
ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);
class_create
device_reate ===>创建设备节点 video0
*/
4.注销
video_unregister_device
user:
open("/dev/video0",) read(fd,buf,sizeof(buf)); ioctl(fd,,) close(fd);
---------------------------------------------------------
cdev:file_opreations:fops :open read write ioctl close
==>字符设备驱动的操作方法结构体
---------------------------------------------------------
v4l2_file_operations:fops :open read write ioctl close
==>上面字符设备驱动的open,read,write,close等函数就会转到
---------------------------------------------------------
v4l2_ioctl_ops :ioctl_ops; //必备的11个ioctl
/*表示它是一个摄像头设备*/
.vidioc_querycap = vidioc_querycap,
/* 用于列举、获得、测试、设置摄像头的数据的格式 */
.vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid_cap,
.vidioc_g_fmt_vid_cap = vidioc_g_fmt_vid_cap,
.vidioc_try_fmt_vid_cap = vidioc_try_fmt_vid_cap,
.vidioc_s_fmt_vid_cap = vidioc_s_fmt_vid_cap,
/* 缓冲区操作: 申请/查询/放入队列/取出队列 */
.vidioc_reqbufs = vidioc_reqbufs,
.vidioc_querybuf = vidioc_querybuf,
.vidioc_qbuf = vidioc_qbuf,
.vidioc_dqbuf = vidioc_dqbuf,
/*启动/停止*/
.vidioc_streamon = vidioc_streamon,
.vidioc_streamoff = vidioc_streamoff,
3.4配置ov5640
3.4.1配置ov5640相关驱动
需要配置的驱动的代码DCMI(摄像头控制器),V4L2的核心,ov5640到内核中
配置V4l2的核心
配置ov5640
配置DCMI驱动
将上述的代码配置到内核之后,重新编译内核 make uImage LOADADDR=0xc2000000 -j4
4.4.2配置设备树
参看内核中的摄像头设备树的参考文件配置即可
修改arch/arm/boot/dts/stm32mp15xx-fsmp1x.dtsi
#include "stm32mp15-m4-srm.dtsi"
#include "stm32mp15-m4-srm-pinctrl.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/mfd/st,stpmic1.h>
#include <dt-bindings/rtc/rtc-stm32.h>
/ {
memory@c0000000 {
device_type = "memory";
reg = <0xc0000000 0x20000000>;
};
v1v8_audio: regulator-v1v8_audio {
compatible = "regulator-fixed";
regulator-name = "v1v8_audio";
regulator-min-microvolt = <1800000>;
regulator-max-microvolt = <1800000>;
regulator-always-on;
regulator-boot-on;
};
sound {
compatible = "audio-graph-card";
label = "STM32MP1-FSMP1A";
routing =
"Playback" , "MCLK",
"Capture" , "MCLK",
"MICL" , "Mic Bias";
dais = <&sai2a_port &sai2b_port>;
status = "okay";
};
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
mcuram2: mcuram2@10000000 {
compatible = "shared-dma-pool";
reg = <0x10000000 0x40000>;
no-map;
};
vdev0vring0: vdev0vring0@10040000 {
compatible = "shared-dma-pool";
reg = <0x10040000 0x1000>;
no-map;
};
vdev0vring1: vdev0vring1@10041000 {
compatible = "shared-dma-pool";
reg = <0x10041000 0x1000>;
no-map;
};
vdev0buffer: vdev0buffer@10042000 {
compatible = "shared-dma-pool";
reg = <0x10042000 0x4000>;
no-map;
};
mcuram: mcuram@30000000 {
compatible = "shared-dma-pool";
reg = <0x30000000 0x40000>;
no-map;
};
retram: retram@38000000 {
compatible = "shared-dma-pool";
reg = <0x38000000 0x10000>;
no-map;
};
};
vin: vin {
compatible = "regulator-fixed";
regulator-name = "vin";
regulator-min-microvolt = <5000000>;
regulator-max-microvolt = <5000000>;
regulator-always-on;
};
v3v3: regulator-3p3v {
compatible = "regulator-fixed";
regulator-name = "v3v3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
regulator-always-on;
regulator-boot-on;
};
vdd: regulator-vdd {
compatible = "regulator-fixed";
regulator-name = "vdd";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
regulator-always-on;
regulator-boot-on;
};
v3v3_hdmi: regulator-v3v3-hdmi {
compatible = "regulator-fixed";
regulator-name = "v3v3_hdmi ";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
regulator-always-on;
regulator-boot-on;
};
v1v2_hdmi: regulator-v1v2-hdmi {
compatible = "regulator-fixed";
regulator-name = "v1v2_hdmi";
regulator-min-microvolt = <1200000>;
regulator-max-microvolt = <1200000>;
regulator-always-on;
regulator-boot-on;
};
panel_backlight: panel-backlight {
compatible = "pwm-backlight";
pwms = <&pwm2 0 5000000>;
brightness-levels = <0 4 8 16 32 64 128 255>;
default-brightness-level = <6>;
status = "okay";
};
v2v8: regulator-2p8v {
compatible = "regulator-fixed";
regulator-name = "v2v8";
regulator-min-microvolt = <2800000>;
regulator-max-microvolt = <2800000>;
regulator-always-on;
regulator-boot-on;
};
clocks {
clk_ext_camera: clk-ext-camera {
#clock-cells = <0>;
compatible = "fixed-clock";
clock-frequency = <24000000>;
};
};
wifi_pwrseq: wifi-pwrseq {
compatible = "mmc-pwrseq-simple";
reset-gpios = <&gpiod 4 GPIO_ACTIVE_LOW>;
};
};
/*HDMI CEC控制器*/
&cec {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&cec_pins_b>;
pinctrl-1 = <&cec_sleep_pins_b>;
status = "okay";
};
/*循环冗余校验计算单元*/
&crc1 {
status = "okay";
};
&dma1 {
sram = <&dma_pool>;
};
&dma2 {
sram = <&dma_pool>;
};
/*数字钟温度传感器*/
&dts {
status = "okay";
};
/*图像处理单元*/
&gpu {
contiguous-area = <&gpu_reserved>;
status = "okay";
};
/*哈希处理器*/
&hash1 {
status = "okay";
};
/*处理器间通信控制器*/
&ipcc {
status = "okay";
};
/*看门狗*/
&iwdg2 {
timeout-sec = <32>;
status = "okay";
};
/*随机数发生器*/
&rng1 {
status = "okay";
};
/*实时时钟*/
&rtc {
st,lsco = <RTC_OUT2_RMP>;
pinctrl-0 = <&rtc_out2_rmp_pins_a>;
pinctrl-names = "default";
status = "okay";
};
/*sdmmc1 TF卡*/
&sdmmc1 {
pinctrl-names = "default", "opendrain", "sleep";
pinctrl-0 = <&sdmmc1_b4_pins_a>;
pinctrl-1 = <&sdmmc1_b4_od_pins_a>;
pinctrl-2 = <&sdmmc1_b4_sleep_pins_a>;
cd-gpios = <&gpioh 3 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
disable-wp;
st,neg-edge;
bus-width = <4>;
vmmc-supply = <&v3v3>;
status = "okay";
};
&sdmmc2 {
pinctrl-names = "default", "opendrain", "sleep";
pinctrl-0 = <&sdmmc2_b4_pins_a &sdmmc2_d47_pins_a>;
pinctrl-1 = <&sdmmc2_b4_od_pins_a &sdmmc2_d47_pins_a>;
pinctrl-2 = <&sdmmc2_b4_sleep_pins_a &sdmmc2_d47_sleep_pins_a>;
non-removable;
no-sd;
no-sdio;
st,neg-edge;
bus-width = <8>;
vmmc-supply = <&v3v3>;
vqmmc-supply = <&vdd>;
mmc-ddr-3_3v;
status = "okay";
};
&sdmmc3 {
arm,primecell-periphid = <0x10153180>;
pinctrl-names = "default", "opendrain", "sleep";
pinctrl-0 = <&sdmmc3_b4_wifi_pins_a>;
pinctrl-1 = <&sdmmc3_b4_od_wifi_pins_a>;
pinctrl-2 = <&sdmmc3_b4_sleep_wifi_pins_a>;
non-removable;
st,neg-edge;
bus-width = <4>;
vmmc-supply = <&v3v3>;
mmc-pwrseq = <&wifi_pwrseq>;
#address-cells = <1>;
#size-cells = <0>;
keep-power-in-suspend;
status = "okay";
brcmf: bcrmf@1 {
reg = <1>;
compatible = "brcm,bcm4329-fmac";
};
};
&sram {
dma_pool: dma_pool@0 {
reg = <0x50000 0x10000>;
pool;
};
};
/*命令行终端*/
&uart4 {
pinctrl-names = "default", "sleep", "idle";
pinctrl-0 = <&uart4_pins_a>;
pinctrl-1 = <&uart4_sleep_pins_a>;
pinctrl-2 = <&uart4_idle_pins_a>;
pinctrl-3 = <&uart4_pins_a>;
/delete-property/dmas;
/delete-property/dma-names;
status = "okay";
};
/*电源基准缓冲器*/
&vrefbuf {
regulator-min-microvolt = <2500000>;
regulator-max-microvolt = <2500000>;
vdda-supply = <&vdd>;
status = "okay";
};
&timers2 {
/* spare dmas for other usage */
/delete-property/dmas;
/delete-property/dma-names;
status = "okay";
pwm2: pwm {
pinctrl-0 = <&pwm2_pins_b>;
pinctrl-1 = <&pwm2_sleep_pins_b>;
pinctrl-names = "default", "sleep";
#pwm-cells = <2>;
status = "okay";
};
timer@2 {
status = "disabled";
};
};
&pinctrl {
sdmmc3_b4_wifi_pins_a: sdmmc3-b4-wifi-0 {
pins1 {
pinmux = <STM32_PINMUX('F', 0, AF9)>, /* SDMMC3_D0 */
<STM32_PINMUX('F', 4, AF9)>, /* SDMMC3_D1 */
<STM32_PINMUX('D', 5, AF10)>, /* SDMMC3_D2 */
<STM32_PINMUX('D', 7, AF10)>, /* SDMMC3_D3 */
<STM32_PINMUX('D', 0, AF10)>; /* SDMMC3_CMD */
slew-rate = <1>;
drive-push-pull;
bias-pull-up;
};
pins2 {
pinmux = <STM32_PINMUX('G', 15, AF10)>; /* SDMMC3_CK */
slew-rate = <2>;
drive-push-pull;
bias-pull-up;
};
};
sdmmc3_b4_od_wifi_pins_a: sdmmc3-b4-od-wifi-0 {
pins1 {
pinmux = <STM32_PINMUX('F', 0, AF9)>, /* SDMMC3_D0 */
<STM32_PINMUX('F', 4, AF9)>, /* SDMMC3_D1 */
<STM32_PINMUX('D', 5, AF10)>, /* SDMMC3_D2 */
<STM32_PINMUX('D', 7, AF10)>; /* SDMMC3_D3 */
slew-rate = <1>;
drive-push-pull;
bias-pull-up;
};
pins2 {
pinmux = <STM32_PINMUX('G', 15, AF10)>; /* SDMMC3_CK */
slew-rate = <2>;
drive-push-pull;
bias-pull-up;
};
pins3 {
pinmux = <STM32_PINMUX('D', 0, AF10)>; /* SDMMC2_CMD */
slew-rate = <1>;
drive-open-drain;
bias-pull-up;
};
};
sdmmc3_b4_sleep_wifi_pins_a: sdmmc3-b4-sleep-wifi-0 {
pins {
pinmux = <STM32_PINMUX('F', 0, ANALOG)>, /* SDMMC3_D0 */
<STM32_PINMUX('F', 4, ANALOG)>, /* SDMMC3_D1 */
<STM32_PINMUX('D', 5, ANALOG)>, /* SDMMC3_D2 */
<STM32_PINMUX('D', 7, ANALOG)>, /* SDMMC3_D3 */
<STM32_PINMUX('G', 15, ANALOG)>, /* SDMMC3_CK */
<STM32_PINMUX('D', 0, ANALOG)>; /* SDMMC3_CMD */
};
};
pwm2_pins_b: pwm2-0 {
pins {
pinmux = <STM32_PINMUX('A', 5, AF1)>; /* TIM2_CH1 */
bias-pull-down;
drive-push-pull;
slew-rate = <0>;
};
};
pwm2_sleep_pins_b: pwm1-sleep-0 {
pins {
pinmux = <STM32_PINMUX('A', 5, ANALOG)>; /* TIM2_CH1 */
};
};
usart3_pins_bt: usart3-bt-0 {
pins1 {
pinmux = <STM32_PINMUX('D', 8, AF7)>, /* USART3_TX */
<STM32_PINMUX('D', 12, AF7)>; /* USART3_RTS */
bias-disable;
drive-push-pull;
slew-rate = <0>;
};
pins2 {
pinmux = <STM32_PINMUX('D', 9, AF7)>, /* USART3_RX */
<STM32_PINMUX('D', 11, AF7)>; /* USART3_CTS_NSS */
bias-disable;
};
};
usart3_idle_pins_bt: usart3-idle-bt-0 {
pins1 {
pinmux = <STM32_PINMUX('D', 8, ANALOG)>, /* USART3_TX */
<STM32_PINMUX('D', 12, ANALOG)>, /* USART3_RTS */
<STM32_PINMUX('D', 11, ANALOG)>; /* USART3_CTS_NSS */
};
pins2 {
pinmux = <STM32_PINMUX('D', 9, AF7)>; /* USART3_RX */
bias-disable;
};
};
usart3_sleep_pins_bt: usart3-sleep-bt-0 {
pins {
pinmux = <STM32_PINMUX('D', 8, ANALOG)>, /* USART3_TX */
<STM32_PINMUX('D', 12, ANALOG)>, /* USART3_RTS */
<STM32_PINMUX('D', 11, ANALOG)>, /* USART3_CTS_NSS */
<STM32_PINMUX('D', 9, ANALOG)>; /* USART3_RX */
};
};
btreg0: bt_reg_on-0 {
pins {
pinmux = <STM32_PINMUX('D', 13, GPIO)>;
drive-push-pull;
bias-pull-up;
output-high;
slew-rate = <0>;
};
};
};
&i2c5 {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&i2c5_pins_a>;
pinctrl-1 = <&i2c5_sleep_pins_a>;
i2c-scl-rising-time-ns = <100>;
i2c-scl-falling-time-ns = <7>;
clock-frequency = <100000>;
/* spare dmas for other usage */
/delete-property/dmas;
/delete-property/dma-names;
status = "okay";
hdmi-transmitter@39 {
compatible = "sil,sii9022";
reg = <0x39>;
iovcc-supply = <&v3v3_hdmi>;
cvcc12-supply = <&v1v2_hdmi>;
reset-gpios = <&gpioa 13 GPIO_ACTIVE_LOW>;
interrupts = <14 IRQ_TYPE_EDGE_FALLING>;
interrupt-parent = <&gpioa>;
#sound-dai-cells = <0>;
status = "okay";
ports {
#address-cells = <1>;
#size-cells = <0>;
port@0 {
reg = <0>;
sii9022_in: endpoint {
remote-endpoint = <<dc_ep0_out>;
};
};
};
};
cs42l51: cs42l51@4a {
compatible = "cirrus,cs42l51";
reg = <0x4a>;
#sound-dai-cells = <0>;
VL-supply = <&v3v3>;
VD-supply = <&v1v8_audio>;
VA-supply = <&v1v8_audio>;
VAHP-supply = <&v1v8_audio>;
reset-gpios = <&gpioc 0 GPIO_ACTIVE_LOW>;
clocks = <&sai2a>;
clock-names = "MCLK";
status = "okay";
cs42l51_port: port {
#address-cells = <1>;
#size-cells = <0>;
cs42l51_tx_endpoint: endpoint@0 {
reg = <0>;
remote-endpoint = <&sai2a_endpoint>;
frame-master;
bitclock-master;
};
cs42l51_rx_endpoint: endpoint@1 {
reg = <1>;
remote-endpoint = <&sai2b_endpoint>;
frame-master;
bitclock-master;
};
};
};
ov5640: camera@3c {
compatible = "ovti,ov5640";
reg = <0x3c>;
clocks = <&clk_ext_camera>;
clock-names = "xclk";
DOVDD-supply = <&v2v8>;
powerdown-gpios = <&gpioa 4 (GPIO_ACTIVE_HIGH | GPIO_PUSH_PULL)>;
reset-gpios = <&gpioa 3 (GPIO_ACTIVE_LOW | GPIO_PUSH_PULL)>;
rotation = <180>;
status = "okay";
port {
ov5640_0: endpoint {
remote-endpoint = <&dcmi_0>;
bus-width = <8>;
data-shift = <2>; /* lines 9:2 are used */
hsync-active = <0>;
vsync-active = <0>;
pclk-sample = <1>;
pclk-max-frequency = <77000000>;
};
};
};
};
<dc {
pinctrl-names = "default", "sleep";
pinctrl-0 = <<dc_pins_b>;
pinctrl-1 = <<dc_sleep_pins_b>;
status = "okay";
port {
#address-cells = <1>;
#size-cells = <0>;
ltdc_ep0_out: endpoint@0 {
reg = <0>;
remote-endpoint = <&sii9022_in>;
};
};
};
&sai2 {
clocks = <&rcc SAI2>, <&rcc PLL3_Q>, <&rcc PLL3_R>;
clock-names = "pclk", "x8k", "x11k";
pinctrl-names = "default", "sleep";
pinctrl-0 = <&sai2a_pins_a>, <&sai2b_pins_b>;
pinctrl-1 = <&sai2a_sleep_pins_a>, <&sai2b_sleep_pins_b>;
status = "okay";
sai2a: audio-controller@4400b004 {
#clock-cells = <0>;
dma-names = "tx";
clocks = <&rcc SAI2_K>;
clock-names = "sai_ck";
status = "okay";
sai2a_port: port {
sai2a_endpoint: endpoint {
remote-endpoint = <&cs42l51_tx_endpoint>;
format = "i2s";
mclk-fs = <256>;
dai-tdm-slot-num = <2>;
dai-tdm-slot-width = <32>;
};
};
};
sai2b: audio-controller@4400b024 {
dma-names = "rx";
st,sync = <&sai2a 2>;
clocks = <&rcc SAI2_K>, <&sai2a>;
clock-names = "sai_ck", "MCLK";
status = "okay";
sai2b_port: port {
sai2b_endpoint: endpoint {
remote-endpoint = <&cs42l51_rx_endpoint>;
format = "i2s";
mclk-fs = <256>;
dai-tdm-slot-num = <2>;
dai-tdm-slot-width = <32>;
};
};
};
};
&dcmi {
status = "okay";
pinctrl-names = "default", "sleep";
pinctrl-0 = <&dcmi_pins_a>;
pinctrl-1 = <&dcmi_sleep_pins_a>;
port {
dcmi_0: endpoint {
remote-endpoint = <&ov5640_0>;
bus-width = <8>;
hsync-active = <0>;
vsync-active = <0>;
pclk-sample = <1>;
pclk-max-frequency = <77000000>;
};
};
};
&usart3 {
pinctrl-names = "default", "sleep", "idle";
pinctrl-0 = <&usart3_pins_bt>;
pinctrl-1 = <&usart3_idle_pins_bt>;
pinctrl-2 = <&usart3_sleep_pins_bt>;
uart-has-rtscts;
status = "okay";
bluetooth {
pinctrl-names = "default";
pinctrl-0 = <&btreg0>;
compatible = "brcm,bcm43438-bt";
max-speed = <3000000>;
vbat-supply = <&v3v3>;
vddio-supply = <&v3v3>;
};
};
&sdmmc3 {
arm,primecell-periphid = <0x10153180>;
pinctrl-names = "default", "opendrain", "sleep";
pinctrl-0 = <&sdmmc3_b4_wifi_pins_a>;
pinctrl-1 = <&sdmmc3_b4_od_wifi_pins_a>;
pinctrl-2 = <&sdmmc3_b4_sleep_wifi_pins_a>;
non-removable;
st,neg-edge;
bus-width = <4>;
vmmc-supply = <&v3v3>;
mmc-pwrseq = <&wifi_pwrseq>;
#address-cells = <1>;
#size-cells = <0>;
keep-power-in-suspend;
status = "okay";
brcmf: bcrmf@1 {
reg = <1>;
compatible = "brcm,bcm4329-fmac";
};
};
ðernet0 {
status = "okay";
pinctrl-0 = <ðernet0_rgmii_pins_a>;
pinctrl-1 = <ðernet0_rgmii_sleep_pins_a>;
pinctrl-names = "default", "sleep";
phy-mode = "rgmii-id";
max-speed = <1000>;
phy-handle = <&phy0>;
mdio0 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "snps,dwmac-mdio";
phy0: ethernet-phy@0 {
reg = <0>;
};
};
};
重新编译设备树make dtbs,此时设备树即可工作
将设备树和内核拷贝到tftpboot目录下
3.4.3查看节点
当上述的内核和设备树编译好之后,在保证摄像头接入的情况下,启动开发板之后就能够看到/dev/video0
的节点
3.4.4执行应用程序拍照
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <string.h>
typedef struct VideoBuffer {
void *start;
size_t length;
} VideoBuffer;
VideoBuffer *buffers;
int camera_device_open(const char * dev)
{
int fd;
//用阻塞模式打开摄像头设备
fd = open(dev,O_RDWR,0);
if(fd < 0){
printf("open %s is fail.\n",dev);
exit(EXIT_FAILURE);
}
return fd;
}
int init_camera_attribute(int fd)
{
int numBufs;
v4l2_std_id id;
struct v4l2_format fmt;
struct v4l2_requestbuffers req;
struct v4l2_buffer buf;
//检查当前视频设备支持的标准
ioctl(fd,VIDIOC_QUERYSTD,&id);
//设置视频捕获格式
memset(&fmt,0,sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
// fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if(ioctl(fd,VIDIOC_S_FMT,&fmt) == -1){
perror("set VIDIOC_S_FMT is fail");
exit(EXIT_FAILURE);
}
//分配内存
memset(&req,0,sizeof(req));
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if(ioctl(fd,VIDIOC_REQBUFS,&req) == -1){
perror("set VIDIOC_REQBUFS is fail");
exit(EXIT_FAILURE);
}
//获取并记录缓存的物理空间
buffers = calloc(req.count,sizeof(*buffers));
for(numBufs = 0; numBufs < req.count; numBufs ++){
memset(&buf,0,sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = numBufs;
//读取缓存
if(ioctl(fd,VIDIOC_QUERYBUF,&buf) == -1){
perror("set VIDIOC_REQBUFS is fail");
exit(EXIT_FAILURE);
}
// 转换成相对地址
buffers[numBufs].length = buf.length;
buffers[numBufs].start = mmap(NULL,buf.length,PROT_READ|PROT_WRITE,
MAP_SHARED,fd,buf.m.offset);
if(buffers[numBufs].start == MAP_FAILED){
perror("mmap is fail");
exit(EXIT_FAILURE);
}
// 放入缓存队列
if(ioctl(fd,VIDIOC_QBUF,&buf) == -1){
perror("set VIDIOC_QBUF is fail");
exit(EXIT_FAILURE);
}
}
return 0;
}
int start_capturing(int fd)
{
enum v4l2_buf_type type;
//开始采集数据
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(ioctl(fd,VIDIOC_STREAMON,&type) == -1){
perror("start capturing is fail");
exit(EXIT_FAILURE);
}
return 0;
}
int build_picture(void *addr,int length)
{
FILE *fp;
static int num=0;
char picture_name[20];
sprintf(picture_name,"picture%d.yuv",num++);
fp = fopen(picture_name,"w");
if(fp == NULL){
perror("fail to open ");
exit(EXIT_FAILURE);
}
fwrite(addr,1,length,fp);
fclose(fp);
return 0;
}
int read_image(int fd)
{
struct v4l2_buffer buf;
memset(&buf,0,sizeof(buf));
buf.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory=V4L2_MEMORY_MMAP;
buf.index=0;
//读取缓存
if(ioctl(fd,VIDIOC_DQBUF,&buf) == -1){
perror("set VIDIOC_DQBUF is fail");
exit(EXIT_FAILURE);
}//将数据存为图片
build_picture(buffers[buf.index].start,buffers[buf.index].length);
//重新放入缓存队列
if(ioctl(fd,VIDIOC_QBUF,&buf) == -1){
perror("reset VIDIOC_QBUF is fail");
exit(EXIT_FAILURE);
}
return 0;
}
int when_to_read(int fd)
{
int i=0;
for(i=0;i<6;i++)
{
fd_set rfds;
struct timeval tv;
int retval;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
tv.tv_sec = 2;
tv.tv_usec = 0;
retval = select(fd+1, &rfds, NULL, NULL, &tv);
if(retval == -1){
perror("select()");
exit(EXIT_FAILURE);
}else if(retval == 0){
printf("select is timeout\n");
}else{
read_image(fd);
}
}
return 0;
}
int stop_capturing(int fd)
{
enum v4l2_buf_type type;
//开始采集数据
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(ioctl(fd,VIDIOC_STREAMOFF,&type) == -1){
perror("stop capturing is fail");
exit(EXIT_FAILURE);
}
return 0;
}
int uninit_camera(int fd)
{
int i;
for(i=0;i<4;i++){
if(-1 == munmap(buffers[i].start,buffers[i].length))
{
perror("munmap is fail");
exit(EXIT_FAILURE);
}
}
free(buffers);
close(fd);
return 0;
}
int main(int argc, const char *argv[])
{
int fd;
fd = camera_device_open(argv[1]);
init_camera_attribute(fd);
start_capturing(fd);
when_to_read(fd);
stop_capturing(fd);
uninit_camera(fd);
return 0;
}
编译之后arm-linux-gnueabihf-gcc ov5640_camera_app.c -o ov5640_camera_app,
执行./ov5640_camera_app /dev/video0
就能够拍摄出来照片了 picture0.yuv - picture5.yuv
3.4.5查看图片
上述的命令在开发板的终端执行完之后,得到了picturex.yuv但是这个图片不能直接打开,
因为没有按照图片的格式来存储,这个文件中存储的是图像的原始数据,如果想查看需
要在Ubuntu上安装如下的工具:
sudo apt-get install ffmpeg
当你安装完这个工具有在Ubuntu上执行
ffplay -pixel_format yuyv422 -video_size 640*480 picture5.yuv