-
Dell 外设驱动
2019-01-17 10:14:43Dell 外设驱动,支持键盘,鼠标,USB等外接设备。。。。。 -
linux外设驱动实现专栏:各虚拟外设驱动代码实现集合
2020-06-22 22:02:49之前在对linux各子驱动系统的专栏分析中,已经实现了虚拟的外设驱动实现,可帮助想学习设备驱动且没有开发板的童鞋提供学习环境,目前实现的虚拟外设驱动均可在ubuntu16.04/ubuntu18.04上运行验证,本篇文章主要将...之前在对linux各子驱动系统的专栏分析中,已经实现了虚拟的外设驱动实现,可帮助想学习设备驱动且没有开发板的童鞋提供学习环境,目前此处实现的虚拟外设驱动均可在ubuntu16.04/ubuntu18.04上运行验证,本篇文章主要将这些虚拟外设驱动的实现整合在一起,方便查阅。
目前已完成了linux下i2c子系统、spi子系统、tty子系统、uart子系统、input子系统、hwmon子系统、gpio子系统、pwm子系统、led子系统、pinctrl子系统中虚拟设备的开发,可以帮助想要学习linux各子模块且没有开发板的童鞋熟悉各子系统的驱动开发流程。虚拟驱动代码均已上传至gitee上,读者可从下面的文章链接内看到对应的gitee路径。
Linux虚拟i2c控制器实现---适用于无开发板学习i2c driver
Linux虚拟spi控制器实现---适用于无开发板学习spi driver
linux虚拟串口控制器实现---适用于无开发板学习tty driver
linux虚拟串口控制器实现---适用于无开发板学习uart driver
linux虚拟input device驱动实现---适用于无开发板学习input driver
Linux虚拟hwmon device驱动实现---适用于无开发板学习hwmon driver
Linux虚拟hwmon device驱动实现---适用于无开发板学习hwmon driver(适用于新版hwmon driver 接口)
linux虚拟gpio控制器实现---适用于无开发板学习gpio driver
Linux虚拟 led设备驱动与ledtrigger驱动实现---适用于无开发板学习led driver
linux虚拟pwm设备驱动实现---适用于无开发板学习pwm driver
Linux虚拟pinctrl dev驱动开发实例---适用于无开发板学习pinctrl 子系统驱动
linux虚拟regulator device驱动实现---适用于无开发板学习regulator子系统驱动
LINUX虚拟CLK driver驱动实现---适用于无开发板学习CCF子系统驱动
LINUX虚拟IRQ CONTROLLER驱动实现---适用于无开发板学习IRQ子系统驱动
以上即为目前已实现的虚拟外设驱动的内容,后续也会不定期更新。
-
BabyOS 外设驱动的框架 v5.0.0
2020-11-25 10:24:57为您提供BabyOS 外设驱动的框架下载,BabyOS适用于MCU项目,她是一套管理功能模块和外设驱动的框架。对项目而言,缩短开发周期。项目开发时选择适用的功能模块及驱动。直接进入功能代码编写的阶段。对工程师而言,... -
STM32常用外设驱动程序
2017-05-01 09:12:18STM32常用外设驱动程序 -
RK3399外设驱动之I2C驱动
2021-04-09 09:37:42RK3399外设驱动之I2C驱动 文章目录RK3399外设驱动之I2C驱动Linux-I2C框架I2C重要结构体I2C总线i2c_bus_typeI2C驱动i2c_driverI2C设备i2c_clientI2C设配器i2c_adapter匹配原则i2c_add_driver注册调用顺序of_driver_...RK3399外设驱动之I2C驱动
内核版本 Linux4.4 平台 rk3399 作者 nineyole 很多从单片机开发的朋友转到Linux开发的时候,刚开始很不习惯,还想着从寄存器的角度来分析整个驱动,然而Linux庞大的架构体系让很多人都望而生畏,本文将仔细从驱动层分析到内核core层,来分析整个驱动开发是如何调用到rk3399的i2c寄存器的,并且举例说明在rk3399 i2c实际工作中遇到的相关问题以及解决方法。
我们都知道i2c驱动有4个非常重要的模块
I2C总线
、I2C驱动
、I2C设备
、I2C设备器
。i2c总线维护着两个非常重要的链表,一个是驱动链表,一个是设备链表,每当注册进一个驱动(或设备),就会将其添加到总线上相应的链表上,然后遍历总线的设备(或驱动)链表的所有设备(或驱动),通过总线的匹配函数判断是够匹配,如果匹配,则调用驱动的probe函数,然后我们就可以在probe函数注册设备,创建设备节点,实现fops等。下面我们将以rk3399平台为例来分别介绍这几个模块。
Linux-I2C框架
一般来说I2C都是一个主设备,然后带很多个从设备。其示意图如下图所示。
I2C重要结构体
I2C总线i2c_bus_type
i2c_bus_type
定义如下:struct bus_type i2c_bus_type = { .name = "i2c", .match = i2c_device_match, .probe = i2c_device_probe, .remove = i2c_device_remove, .shutdown = i2c_device_shutdown, };
在i2c_init中会调用下面函数进行注册
retval = bus_register(&i2c_bus_type);
I2C总线同时对应着/bus下的一条总线,这个i2c_bus_type总线结构体管理着i2c设备与I2C驱动的匹配、探测、移除等操作。当调用i2c_add_driver进行i2c设备注册的时候,就会最终调用drv->bus->match(dev, drv),即i2c_device_match函数,来查看I2C设备和I2C驱动是否匹配,如果匹配就调用status = driver->probe(client, i2c_match_id(driver->id_table, client));调用driver注册时候生命的probe函数。
可以通过cat查看当前注册了哪些设备。
rk3399_all:/ # cat sys/bus/i2c/devices/ 0-001b/ 0-0041/ 1-001a/ 2-0018/ 4-0022/ 4-006b/ i2c-0/ i2c-2/ i2c-5/ 0-0040/ 1-0010/ 2-0009/ 2-0037/ 4-0062/ 5-0014/ i2c-1/ i2c-4/ i2c-9/
I2C驱动i2c_driver
我们在注册一个i2c驱动的时候就需要填充这个结构体,并且跟i2c设备进行绑定。
struct i2c_driver { /* Standard driver model interfaces */ int (*probe)(struct i2c_client *, const struct i2c_device_id *); int (*remove)(struct i2c_client *); /* driver model interfaces that don't relate to enumeration */ void (*shutdown)(struct i2c_client *); int (*command)(struct i2c_client *client, unsigned int cmd, void *arg); struct device_driver driver; const struct i2c_device_id *id_table; /* Device detection callback for automatic device creation */ int (*detect)(struct i2c_client *, struct i2c_board_info *); const unsigned short *address_list; struct list_head clients; };
I2C设备i2c_client
I2C设备,就是具体的一个设备,比如触摸屏、电量计,电源、陀螺仪等设备,都是由i2c_client结构体来负责维护的。
struct i2c_client { unsigned short flags; /* div., see below */ unsigned short addr; /* chip address - NOTE: 7bit */ /* addresses are stored in the */ /* _LOWER_ 7 bits */ char name[I2C_NAME_SIZE];//设备名称 struct i2c_adapter *adapter; /* the adapter we sit on */ struct device dev; /* the device structure */ int irq; /* irq issued by device */ struct list_head detected; };
在我们进入具体的设备的probe函数的时候都会传递一个i2c_client的结构体过来,这个结构体是如何传递过来的呢?比如针对gt9xx的触摸屏驱动:
static int goodix_ts_probe(struct i2c_client *client, const struct i2c_device_id *id)
在
i2c_device_probe
中有如下代码:而i2c_device_probe
中会调用goodix_ts_probe函数,因此这个goodix_ts_probe的i2c_client就来自于下面的代码:struct i2c_client *client = i2c_verify_client(dev);
而这个dev就是和对应dts中配置的i2c设备中的匹配起来的,包括地址,clk等参数。
I2C设配器i2c_adapter
用于I2C驱动和I2C设备间的通信,是针对CPU上
I2C控制器
的一个具体实现。其定义如下:struct i2c_adapter { struct module *owner; unsigned int class; /* classes to allow probing for */ const struct i2c_algorithm *algo; /* the algorithm to access the bus */ void *algo_data; /* data fields that are valid for all devices */ struct rt_mutex bus_lock; int timeout; /* in jiffies */ int retries; struct device dev; /* the adapter device */ int nr; char name[48]; struct completion dev_released; struct mutex userspace_clients_lock; struct list_head userspace_clients; struct i2c_bus_recovery_info *bus_recovery_info; const struct i2c_adapter_quirks *quirks; };
其中
i2c_algorithm
是算法的意思,怎么理解呢,就相当于他是用来实现如何通信的。比如i2c的start stop等都是通过i2c_algorithm中的对应来实现的。如果你想自己写cpu的驱动,就只需要去实现这个master_xfer
的函数即可。struct i2c_algorithm { int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,int num);//主设备时候的发送函数 int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,//从设备时候的发送函数 unsigned short flags, char read_write, u8 command, int size, union i2c_smbus_data *data); /* To determine what the adapter supports */ u32 (*functionality) (struct i2c_adapter *); };
匹配原则
i2c_add_driver注册调用顺序
i2c_add_driver//注册i2c的driver -->i2c_register_driver//调用系统接口 -->driver_register(&driver->driver);//调用driver_register -->ret = bus_add_driver(drv);//将driver添加大到bus -->error = driver_attach(drv); -->bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
其中__driver_attach的定义如下:driver_match_device匹配之后,就会执行driver_probe_device函数。即我们需要使用的probe函数。在这个probe函数中实现注册设备,创建设备节点,实现fops等。
static int __driver_attach(struct device *dev, void *data) { struct device_driver *drv = data; if (!driver_match_device(drv, dev))/*判定当前是否匹配,如果driver_match_device返回1表示匹配成功。返回0表示匹配失败*/ return 0; if (dev->parent) /* Needed for USB */ device_lock(dev->parent); device_lock(dev); if (!dev->driver) driver_probe_device(drv, dev);//执行probe函数 device_unlock(dev); if (dev->parent)/* Needed for USB */ device_unlock(dev->parent); return 0; }
driver_match_device函数定义如下:
static inline int driver_match_device(struct device_driver *drv, struct device *dev) { return drv->bus->match ? drv->bus->match(dev, drv) : 1;//调用总线的macth函数 }
上面这个drv->bus->match,就是前面
i2c_bus_type
所指向的i2c_device_match
,其函数定义如下:这里定义了三种匹配方式,在老的Linux内核中采用的是i2c_match_id,而最新的系统都是使用的是of_driver_match_device进行匹配的。static int i2c_device_match(struct device *dev, struct device_driver *drv) { /* Attempt an OF style match */ if (of_driver_match_device(dev, drv))//of_driver_match_device匹配 return 1;/*返回1表示匹配成功。返回0表示匹配失败*/ /* Then ACPI style match */ if (acpi_driver_match_device(dev, drv)) return 1; driver = to_i2c_driver(drv); /* match on an id table if there is one */ if (driver->id_table) return i2c_match_id(driver->id_table, client) != NULL;//调用i2c_match_id进行匹配 return 0; }
of_driver_match_device分析
在上面的代码中,优先是采用的of_driver_match_device匹配规则,而最后才是i2c_match_id匹配,整个of_driver_match_device调用的函数顺序如下图所示:
of_driver_match_device -->of_match_device -->__of_match_node -->__of_device_is_compatible
__of_device_is_compatible的函数定义如下:Returns 0 表示没有匹配, 一个int数据表示匹配成功。匹配的角度包括compatible && type && name正常来说我们一般匹配的是compatible。
static int __of_device_is_compatible(const struct device_node *device, const char *compat, const char *type, const char *name) { if (compat && compat[0]) { prop = __of_find_property(device, "compatible", NULL);//从dts中获取compatible的名字 for (cp = of_prop_next_string(prop, NULL); cp; cp = of_prop_next_string(prop, cp), index++) { if (of_compat_cmp(cp, compat, strlen(compat)) == 0) { score = INT_MAX/2 - (index << 2); break; } } } /* Matching type is better than matching name */ if (type && type[0]) {//对type进行匹配判断 if (!device->type || of_node_cmp(type, device->type)) return 0; score += 2; } /* Matching name is a bit better than not */ if (name && name[0]) {//对name进行匹配判断 if (!device->name || of_node_cmp(name, device->name)) return 0; score++; } return score; }
到此,通过
i2c_add_driver
注册之后,然后调用到匹配函数,匹配之后就会执行driver_probe_device,可以参考__device_attach_driver
函数。probe函数调用逻辑分析
通过前面分析可以知道,当match通过之后就会调用driver_probe_device函数,其调用逻辑如下:
driver_probe_device(drv, dev); -->ret = really_probe(dev, drv); -->ret = dev->bus->probe(dev); -->status = driver->probe(client, i2c_match_id(driver->id_table, client));//driver注册时候生命的probe函数。
driver_probe_device调用really_probe,然后调用到dev->bus->probe。其中dev->bus->probe就是前面讲的i2c_device_probe
通过status = driver->probe(client, i2c_match_id(driver->id_table, client));调用driver注册时候生命的probe函数。
rk3399 i2c驱动代码
rk3399 i2c驱动从注册到调用
本章将仔细从驱动层分析到内核core层,来分析整个驱动开发是如何调用到rk3399的i2c寄存器进行读写的。
i2c-rk3x.c中有如下的rk3x_i2c_driver驱动代码,这个其实就是注册了一个
static struct platform_driver rk3x_i2c_driver = { .probe = rk3x_i2c_probe, .remove = rk3x_i2c_remove, .driver = { .name = "rk3x-i2c", .of_match_table = rk3x_i2c_match, .pm = &rk3x_i2c_pm_ops, }, }; module_platform_driver(rk3x_i2c_driver);
比如在rk3399.dtsi中有如下的定义:
i2c1: i2c@ff110000 { compatible = "rockchip,rk3399-i2c"; reg = <0x0 0xff110000 0x0 0x1000>; clocks = <&cru SCLK_I2C1>, <&cru PCLK_I2C1>; clock-names = "i2c", "pclk"; interrupts = <GIC_SPI 59 IRQ_TYPE_LEVEL_HIGH 0>; pinctrl-names = "default"; pinctrl-0 = <&i2c1_xfer>; #address-cells = <1>; #size-cells = <0>; status = "disabled"; };
在rk3x_i2c_probe函数中,注册了rk3x_i2c_irq函数,用来响应start、read、write、stop的。
ret = devm_request_irq(&pdev->dev, irq, rk3x_i2c_irq, 0, dev_name(&pdev->dev), i2c);
当用户调用了了__i2c_transfer进行数据发送的时候就会调用master_xfer从而调用rk3x_i2c_xfer。
rk3x_i2c_xfer的函数定义如下:我们把函数进行了删减,只留下核心代码。
static int rk3x_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) { for (i = 0; i < num; i += ret) { ret = rk3x_i2c_setup(i2c, msgs + i, num - i);//调用setup函数,并确定是要发送还是接受 if (ret < 0) { dev_err(i2c->dev, "rk3x_i2c_setup() failed\n"); break; } rk3x_i2c_start(i2c);//调用start函数,产生中断。 spin_unlock_irqrestore(&i2c->lock, flags); } }
rk3x_i2c_xfer产生REG_INT_START中断之后就会进入rk3x_i2c_probe函数中注册的rk3x_i2c_irq中断,在中断中会对i2c->state进行判定,并执行对应的,start、read、write、stop等。rk3x_i2c_irq定义如下:
static irqreturn_t rk3x_i2c_irq(int irqno, void *dev_id) { switch (i2c->state) { case STATE_START: rk3x_i2c_handle_start(i2c, ipd);//处理start中断 break; case STATE_WRITE: rk3x_i2c_handle_write(i2c, ipd);//处理写中断 break; case STATE_READ: rk3x_i2c_handle_read(i2c, ipd);//处理读中断 break; case STATE_STOP: rk3x_i2c_handle_stop(i2c, ipd);//处理stop中断 break; case STATE_IDLE: break; } return IRQ_HANDLED; }
设备驱动read函数调用逻辑关系
以下是gt9xx触摸屏的调用逻辑关系:
gtp_i2c_read -->i2c_transfer -->ret = __i2c_transfer(adap, msgs, num); -->ret = adap->algo->master_xfer(adap, msgs, num); -->rk3x_i2c_irq -->rk3x_i2c_handle_start -->rk3x_i2c_handle_read -->val = i2c_readl(i2c, RXBUFFER_BASE + (i / 4) * 4);
在 i2c-rk3x.c中有如下的定义:
static const struct i2c_algorithm rk3x_i2c_algorithm = { .master_xfer = rk3x_i2c_xfer, .functionality = rk3x_i2c_func, };
rk3x_i2c_xfer函数中会调用
rk3x_i2c_start(i2c)
产生一个start 的中断,从而会进入rk3x_i2c_irq中断函数,并且最终调用rk3x_i2c_handle_read函数。rk3x_i2c_handle_read的函数定义如下:
static void rk3x_i2c_handle_read(struct rk3x_i2c *i2c, unsigned int ipd) { ... /* ack interrupt */ i2c_writel(i2c, REG_INT_MBRF, REG_IPD);//可以参考rk3399 TRM 这里就是清除I2C接收中断 /* Can only handle a maximum of 32 bytes at a time */ if (len > 32)//最大只能4*8=32字节 len = 32; /* read the data from receive buffer */ for (i = 0; i < len; ++i) { if (i % 4 == 0) val = i2c_readl(i2c, RXBUFFER_BASE + (i / 4) * 4);//#define RXBUFFER_BASE 0x200 从缓冲区读取数据 byte = (val >> ((i % 4) * 8)) & 0xff; i2c->msg->buf[i2c->processed++] = byte; } /* are we finished? */ if (i2c->processed == i2c->msg->len) rk3x_i2c_stop(i2c, i2c->error);//接收完成,发送stop信号完成当前传送。 else rk3x_i2c_prepare_read(i2c);//清空等待下次继续接收 }
同时在rk3399的TRM中可以看到RXbuf是从0x0200的偏移地址开始的,因此一个完整的数据读取链条就打通了,从设备调用gtp_i2c_read到i2c_readl(i2c, RXBUFFER_BASE + (i / 4) * 4),并最终到底层地址。可以从中看出,rk3399的i2c最多有8个接收寄存器,每个缓冲区4个字节,因此一次最大只能32字节。
设备驱动write函数调用关系
一般来说在i2c的驱动中,write函数调用的还是比较少的,主要用来写入配置参数信息等。下面将从驱动层解析到rkk3399底层寄存器的逻辑。
i2c_write_bytes -->gup_i2c_write -->i2c_transfer(client->adapter, &msg, 1); -->ret = __i2c_transfer(adap, msgs, num); -->ret = adap->algo->master_xfer(adap, msgs, num); -->rk3x_i2c_irq -->rk3x_i2c_setup
在rk3x_i2c_setup中有如下的代码:当在msg设置为I2C_M_WR的时候,就会设置i2c->mode = REG_CON_MOD_TX;从而在rk3x_i2c_handle_start中就会去填充需要发送的数据。
if (msgs[0].flags & I2C_M_RD) { addr |= 1; /* set read bit *//* * We have to transmit the slave addr first. Use * MOD_REGISTER_TX for that purpose. */ i2c->mode = REG_CON_MOD_REGISTER_TX; i2c_writel(i2c, addr | REG_MRXADDR_VALID(0), REG_MRXADDR); i2c_writel(i2c, 0, REG_MRXRADDR); } else { i2c->mode = REG_CON_MOD_TX; }
填充的函数如下:
static void rk3x_i2c_fill_transmit_buf(struct rk3x_i2c *i2c) { ... i2c_writel(i2c, val, TXBUFFER_BASE + 4 * i); if (i2c->processed == i2c->msg->len) break; i2c_writel(i2c, cnt, REG_MTXCNT); }
可以看到最终调用了i2c_writel(i2c, val, TXBUFFER_BASE + 4 * i);其中TXBUFFER_BASE在rk3399的规格书中定义如下:可以看到一共有9个缓冲区,每个缓冲区有4个字节,因此单次最大发送32字节。
I2C调试方法
关于I2C tools
I2C tool 是一个开源工具,需自行下载进行交叉编译,代码下载地址:
https://www.kernel.org/pub/software/utils/i2c‐tools/
或者
git clone git://git.kernel.org/pub/scm/utils/i2c‐tools/i2c‐tools.git
编译后会生成 i2cdetect,i2cdump,i2cset,i2cget 等工具,可以直接在命令行上调试使用:
i2cdetect – 用来列举 I2C bus 和上面所有的设备
i2cdump – 显示 i2c 设备所有 register 的值
i2cget – 读取 i2c 设备某个 register 的值
i2cset – 写入 i2c 设备某个 register 的值
设备NACK问题排查
我们在调试I2C设备的时候,第一步需要保证i2c地址正确,但是如何保证呢,下面是之前在调试的时候通过逻辑分析仪抓取到的波形,可以明显看到发送地址6E之后,返回了NACK,可以明确到设备通信失败了。
如果i2c返回nack,则有两种情况:第一种就是i2c地址错误,如何排查呢?
上面显示的波形是0x6E的数据,但是对于瑞芯微来说,实际我们需要通过以下方式来探测波形:
root@rk3288:/ # echo 0 > /dev/i2c_detect [ 323.414047] I2c0 slave list: 0x1b
这个主要原因是因为瑞芯微的地址和实际地址有偏差,实际给定的地址是0x6E,而用探测出来的地址是0x37。因此瑞芯微的地址相当于是需要0x6E>>1=0x37.之前调试也碰到同样的问题。我们在探测的时候只需要使用下面的方式就可以探测到对应的地址。
root@rk3288:/ # echo 1 > /dev/i2c_detect [ 365.542976] I2c1 slave list: 0x37
修改之后,就可以探测到返回的ACK相关的波形,然后修改了dts的配置,将0x6E改为0x37就可以正常获取到相关的数据点,可以正常读写。
正常来说,我们还是需要深究一下原因的,跟踪了一下源代码,在 i2c-rk3x.c中的rk3x_i2c_setup有如下的定义,可以看到其将地址左移了一位,因此我们在填写dts的时候需要将i2c实际地址右移一位。
static int rk3x_i2c_setup(struct rk3x_i2c *i2c, struct i2c_msg *msgs, int num) { u32 addr = (msgs[0].addr & 0x7f) << 1; }
因此针对调用 I2C 传输接口返回值为 -6(-ENXIO)时候,表示为 NACK 错误,即对方设备无应答响应,这种情况一 般为外设的问题,常见的有以下几种情况:
- I2C 地址错误;
- I2C slave 设备处于不正常工作状态,比如没有上电,错误的上电时序以及设备异常等;
- I2C 时序不符合slave设备所要求也会产生 NACK 信号,比如 slave 设备需要的是 stop 信号,而不是 repeat start 信号的时候;
- I2C 总线受外部干扰导致的,用示波器测量可以看到是一个ACK波形。
i2c通信出现timeout
以下类容来自rk官方:
当出现 I2C 的 log:"timeout, ipd: 0x00, state: 1"时,此时 I2C 控制器工作异常,无法产生中断状态,start 时序无法发出,有以下几种可能:
- I2C SCL或者SDA Pin 脚iomux错误;
- I2C 的上拉电压不对,如电压不够或者上拉电源没有等;
- I2C Pin 脚被外设拉住,电压不对;
- I2C 时钟未开,或者时钟源太小;
- I2C 同时配置了CON_START 和 CON_STOP 位。
当出现 I2C 的 log:"timeout, ipd: 0x10, state: 1"时,此时 I2C 控制器工作正常,但是 cpu 无法响应 I2C 中 断,此时可能cpu0被阻塞了(一般 I2C 中断都在 cpu0上面,通过cat /proc/interrups 可以查看),或者可能 是 I2C 中断位被关闭了。
当出现 I2C 的 log 类似:"timeout, ipd: 0x80, state: 1"时,看到 ipd 为 0x80 打印,可以说明当前 SCL 被 slave 拉住,要判断被哪个 slave 拉住:
一是排除法,适用于外设不多的情况,而且复现概率高;
二是需要修改硬件,在 SCL 总线上串入电阻,通过电阻两端产生的压差来确定,电压更低的那端外设为拉低的 slave,电阻的选取以不影响 I2C 传输且可以看出压差为标准,一般上拉电阻的1/20 以上都可以,如果是 host 拉低也可以看出。另外在此基础上通过示波器来抓取波形更加直观,比较不同 slave 和 host 的低电平大小,与最后出问题时的低电平大小比较,相等的就是拉低总线的”元凶“。 遇到比较多的情况是sda被拉低,证明是谁拉低的,同样参考上面 “SCL 被拉低" 的方法两种。
-
stm32f407部分外设驱动
2017-12-07 21:38:14stm32f407部分外设驱动,can总线,定时器,gpio,串口,系统时钟等 -
dsPIC33E 内部Flash读写及其它外设驱动
2018-12-01 15:14:37dsPIC33E 内部Flash读写及其它外设驱动,使用芯片为dsPIC33EP256GP506,文章地址:https://blog.csdn.net/u010875635/article/details/84673935 -
Linux主机驱动与外设驱动分离思想
2017-11-01 23:59:001主机、外设驱动分离的意义 在Linux设备驱动框架的设计中,除了有分层设计实现以外,还有分隔的思想。举一个简单的例子,假设我们要通过SPI总线访问某外设,在这个访问过程中,要通过操作CPU XXX上的SPI控制器的...- by 宋宝华(Barry Song)
1主机、外设驱动分离的意义在Linux设备驱动框架的设计中,除了有分层设计实现以外,还有分隔的思想。举一个简单的例子,假设我们要通过SPI总线访问某外设,在这个访问过程中,要通过操作CPU XXX上的SPI控制器的寄存器来达到访问SPI外设YYY的目的,最简单的方法是:
return_type xxx_write_spi_yyy(...)
{
xxx_write_spi_host_ctrl_reg(ctrl);
xxx_ write_spi_host_data_reg(buf);
while(!(xxx_spi_host_status_reg()&SPI_DATA_TRANSFER_DONE));
...
}
如果按照这种方式来设计驱动,结果是对于任何一个SPI外设来讲,它的驱动代码都是CPU相关的。也就是说,当然用在CPU XXX上的时候,它访问XXX的SPI主机控制寄存器,当用在XXX1的时候,它访问XXX1的SPI主机控制寄存器:
return_type xxx1_write_spi_yyy(...)
{
xxx1_write_spi_host_ctrl_reg(ctrl);
xxx1_ write_spi_host_data_reg(buf);
while(!(xxx1_spi_host_status_reg()&SPI_DATA_TRANSFER_DONE));
...
}
这显然是不能接受的,因为这意味着外设YYY用在不同的CPU XXX和XXX1上的时候需要不同的驱动。那么,我们可以用如图12.4的思想对主机控制器驱动和外设驱动进行分离。这样的结构是,外设a、b、c的驱动与主机控制器A、B、C的驱动不相关,主机控制器驱动不关心外设,而外设驱动也不关心主机,外设只是访问核心层的通用的API进行数据传输,主机和外设之间可以进行任意的组合。
图12.4 Linux设备驱动的主机、外设驱动分离
如果我们不进行如图12.4的主机和外设分离,外设a、b、c和主机A、B、C进行组合的时候,需要9个不同的驱动。设想一共有m个主机控制器,n个外设,分离的结果是需要m+n个驱动,不分离则需要m*n个驱动。
Linux SPI、I2C、USB、ASoC(ALSA SoC)等子系统都典型地利用了这种分离的设计思想,在本章我们先以简单一些的SPI为例,而I2C、USB、ASoC等则在后续章节会进行详细介绍。
2 Linux SPI主机和设备驱动SPI(同步外设接口)是由摩托罗拉公司开发的全双工同步串行总线,其接口由MISO(串行数据输入),MOSI(串行数据输出),SCK(串行移位时钟),SS(从使能信号)四种信号构成,SS决定了唯一的与主设备通信的从设备,主设备通过产生移位时钟来发起通讯。通讯时,数据由MOSI输出,MISO输入,数据在时钟的上升或下降沿由MOSI输出,在紧接着的下降或上升沿由MISO读入,这样经过8/16次时钟的改变,完成8/16位数据的传输。
SPI模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性(CPOL)和相位(CPHA)可以进行配置。如果 CPOL=0,串行同步时钟的空闲状态为低电平;如果CPOL=1,串行同步时钟的空闲状态为高电平。如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。SPI接口时序如图12.5所示。
图12.5 SPI总线时序
在Linux中,用代码清单12.12的spi_master结构体来描述一个SPI主机控制器驱动,其主要成员是主机控制器的序号(系统中可能存在多个SPI主机控制器)、片选数量、SPI模式和时钟设置用到的函数、数据传输用到的函数等。
代码清单12.12 spi_master结构体
1 struct spi_master {
2 struct device dev;
3 s16 bus_num;
4 u16 num_chipselect;
5
6 /* 设置模式和时钟 */
7 int (*setup)(struct spi_device *spi);
8
9 /* 双向数据传输 */
10 int (*transfer)(struct spi_device *spi,
11 struct spi_message *mesg);
12
13 void (*cleanup)(struct spi_device *spi);
14 };
分配、注册和注销SPI主机的API由SPI核心提供:
struct spi_master * spi_alloc_master(struct device *host, unsigned size);
int spi_register_master(struct spi_master *master);
void spi_unregister_master(struct spi_master *master);
在Linux中,用代码清单12.13的spi_driver结构体来描述一个SPI外设驱动,可以认为是spi_master的client驱动。
代码清单12.13 spi_driver结构体
1 struct spi_driver {
2 int (*probe)(struct spi_device *spi);
3 int (*remove)(struct spi_device *spi);
4 void (*shutdown)(struct spi_device *spi);
5 int (*suspend)(struct spi_device *spi, pm_message_t mesg);
6 int (*resume)(struct spi_device *spi);
7 struct device_driver driver;
8 };
可以看出,spi_driver结构体和platform_driver结构体有极大的相似性,都有probe()、remove()、suspend()、resume()这样的接口。是的,这几乎是一切client驱动的习惯模板。
在SPI外设驱动中,当透过SPI总线进行数据传输的时候,使用了一套与CPU无关的统一的接口。这套接口的第1个关键数据结构就是spi_transfer,它用于描述SPI传输,如代码清单12.14。
代码清单12.14 spi_transfer结构体
1 struct spi_transfer {
2 const void *tx_buf;
3 void *rx_buf;
4 unsigned len;
5
6 dma_addr_t tx_dma;
7 dma_addr_t rx_dma;
8
9 unsigned cs_change:1;
10 u8 bits_per_word;
11 u16 delay_usecs;
12 u32 speed_hz;
13
14 struct list_head transfer_list;
15 };
而一次完整的SPI传输流程可能不只包含1次spi_transfer,它可能包含1个或多个spi_transfer,这些spi_transfer最终通过spi_message组织在一起,其定义如代码清单12.15。
代码清单12.15 spi_message结构体
1 struct spi_message {
2 struct list_head transfers;
3
4 struct spi_device *spi;
5
6 unsigned is_dma_mapped:1;
7
8 /* 完成被一个callback报告 */
9 void (*complete)(void *context);
10 void *context;
11 unsigned actual_length;
12 int status;
13
14 struct list_head queue;
15 void *state;
16 };
通过spi_message_init()可以初始化spi_message,而将spi_transfer添加到spi_message队列的方法则是:
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);
发起一次spi_message的传输有同步和异步两种方式,使用同步API时,会阻塞等待这个消息被处理完。同步操作时使用的API是:
int spi_sync(struct spi_device *spi, struct spi_message *message);
使用异步API时,不会阻塞等待这个消息被处理完,但是可以在spi_message的complete字段挂接一个回调函数,当消息被处理完成后,该函数会被调用。异步操作时使用的API是:
int spi_async(struct spi_device *spi, struct spi_message *message);
代码清单12.16是非常典型的初始化spi_transfer、spi_message并进行SPI数据传输的例子,同时它们也是SPI核心层的2个通用API,在SPI外设驱动中可以直接调用它们进行写和读操作。
代码清单12.16 SPI传输实例spi_write()、spi_read() API
1 static inline int
2 spi_write(struct spi_device *spi, const u8 *buf, size_t len)
3 {
4 struct spi_transfer t = {
5 .tx_buf = buf,
6 .len = len,
7 };
8 struct spi_message m;
9
10 spi_message_init(&m);
11 spi_message_add_tail(&t, &m);
12 return spi_sync(spi, &m);
13 }
14
15 static inline int
16 spi_read(struct spi_device *spi, u8 *buf, size_t len)
17 {
18 struct spi_transfer t = {
19 .rx_buf = buf,
20 .len = len,
21 };
22 struct spi_message m;
23
24 spi_message_init(&m);
25 spi_message_add_tail(&t, &m);
26 return spi_sync(spi, &m);
27 }
LDD6410开发板所使用的S3C6410的SPI主机控制器驱动位于drivers/spi/spi_s3c.h和drivers/spi/spi_s3c.c这2个文件,其主体是实现了spi_master的setup()、transfer()等成员函数。
SPI外设驱动遍布于内核的drivers、sound的各个子目录之下,SPI只是一种总线,spi_driver的作用只是将SPI外设挂接在该总线上,因此在spi_driver的probe()成员函数中,将注册SPI外设本身所属设备驱动的类型。
和platform_driver对应着一个platform_device一样,spi_driver也对应着一个spi_device;platform_device需要在BSP的板文件中添加板信息数据,而spi_device也同样需要。spi_device的板信息用spi_board_info结构体描述,该结构体记录SPI外设使用的主机控制器序号、片选序号、数据比特率、SPI传输模式(即CPOL、CPHA)等。如诺基亚770上2个SPI设备的板信息数据如代码清单12.17,位于板文件arch/arm/mach-omap1/board-nokia770.c。
代码清单12.17诺基亚770板文件中的spi_board_info
1 static struct spi_board_info nokia770_spi_board_info[] __initdata = {
2 [0] = {
3 .modalias = "lcd_mipid",
4 .bus_num = 2, /* 用到的SPI主机控制器序号 */
5 .chip_select = 3, /* 使用哪一号片选 */
6 .max_speed_hz = 12000000, /* SPI数据传输比特率 */
7 .platform_data = &nokia770_mipid_platform_data,
8 },
9 [1] = {
10 .modalias = "ads7846",
11 .bus_num = 2,
12 .chip_select = 0,
13 .max_speed_hz = 2500000,
14 .irq = OMAP_GPIO_IRQ(15),
15 .platform_data = &nokia770_ads7846_platform_data,
16 },
17 };
在Linux启动过程中,在机器的init_machine()函数中,会通过如下语句注册这些spi_board_info:
spi_register_board_info(nokia770_spi_board_info,
ARRAY_SIZE(nokia770_spi_board_info));
这一点和启动时通过platform_add_devices()添加platform_device非常相似。
本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/333312,如需转载请自行联系原作者
-
openwrt笔记6 外设驱动
2019-10-09 15:20:18openwrt笔记6 外设驱动 gpio MT7620a 结合mt7620的datasheet里GPIO pin share schemes以及在mt7620n.dtsi里我们看到有,将GPIO#0到GPIO#72(中间有仅仅做GPO或GPI的)分为四组GPIO0-GPIO3; 对应GPIO0是从GPIO#0...openwrt笔记6 外设驱动
gpio
MT7620a
结合mt7620的datasheet里GPIO pin share schemes以及在mt7620n.dtsi里我们看到有,将GPIO#0到GPIO#72(中间有仅仅做GPO或GPI的)分为四组GPIO0-GPIO3; 对应GPIO0是从GPIO#0开始到GPIO#23,一共有24个; 对应GPIO1是从GPIO#24开始到GPIO#39,一共有16个; 对应GPIO2是从GPIO#40开始到GPIO#71,一共有32个; 对应GPIO3对应的是GPIO#72,仅有一个。
控制台操作IO
终端进入/sys/class/gpio/目录,导出想要控制的GPIO,比如导出GPIO14,则输入以下命令:# cd /sys/class/gpio/ # echo 14 > export # ls
此时是不是发现多了一个gpio14的文件夹?如果没有的话,检查一下上述步骤是不是漏掉了什么。
设置相应IO方向
进入相应文件夹,可能是以下样子的:root@Widora:/sys/devices/10000000.palmbus/10000600.gpio/gpio/gpio14# ls active_low device direction edge subsystem uevent value
设置GPIO方向,支持in和out,比如将gpio14设置为输出:
# echo out > direction''
输出高低电平
设置为高电平# echo 1 > value
设置为低电平
# echo 0 > value
输入测试
设置为输入:root@Widora:/sys/devices/10000000.palmbus/10000600.gpio/gpio/gpio14# echo in > direction
接下来使用杜邦线段路GPIO14和GND后,读取电平:
root@Widora:/sys/devices/10000000.palmbus/10000600.gpio/gpio/gpio14# cat value 0
接下来使用杜邦线段路GPIO14和3V3后,读取电平:
root@Widora:/sys/devices/10000000.palmbus/10000600.gpio/gpio/gpio14# cat value 1
额外注意
目前手中的7688手册是1.4版本,该版本对于GPIO的描述有误导,这些地方的GPO,实测都是GPIO。:gpio 目录下默认属性文件用途 文件名 路径 作用 export /sys/class/gpio/export 导出 GPIO unexport /sys/class/gpio/unexport 将导出的 GPIO 从 sysfs 中清除 gpiochipN /sys/class/gpio/gpiochipN/base 设备所管理的 GPIO 初始编号 /sys/class/gpio/gpiochipN/label 设备信息 /sys/class/gpio/gpiochipN/ngpio 设备所管理的 GPIO 总数 /sys/class/gpio/gpiochipN/power 设备供电方面的相关信息 /sys/class/gpio/gpiochipN/subsystem 符号链接,指向父目录 /sys/class/gpio/gpiochipN/uevent 内核与 udev(自动设备发现程序)之间的通信接口 向 export 文件写入需要操作的 GPIO 排列序号 N,就可以导出对应的 GPIO 设备目录。 操作命令如下: # echo N > /sys/class/gpio/export 例如,导出序号为 68 的 GPIO 的操作接口,在 Shell 下,可以用如下命令: # echo 68 > /sys/class/gpio/export 通过以上操作后在/sys/class/gpio 目录下生成 gpioN 目录,通过读写该设备目录下的属 广州致远电子股份有限公司(www.zlg.cn)/广州周立功单片机科技有限公司(www.zlgmcu.com) 381 性文件(位于 gpioN 下)就可以操作这个 GPIO 的输入和输出。以此类推可以导出其它 GPIO
修改DTS
修改设备树是openwrt比较难的一部分
修改设备树需要根据设备的规格书来进行修改,一般设备的datasheet会告诉
你哪些脚位是有什么复用功能golang库
目前因为sqlite和mysql使用起来比较麻烦,所以使用levelDB作为持久化存储方针,
另外操作gpio有一个很好的库,不需要每次都export io口,只需要运行就会自动导出openwrt patch的使用
openwrt 32M reboot失败
我买的MT7688模块,千辛万苦编译好了一个lede17.01版本烧录进去了,然后写程序用到重启
发现reboot之后一直卡死;然后找度娘发现是以下原因:OpenWrt的最新kernel(3.14.28)已经能够支持32M SPI Flash的读写以及擦除操作. 然而,可能是系统考虑不周,亦或是MT7620系统的BUG,在配置了W25Q256的MT7620开发板系统上, 无法soft reset!经过查阅相关资料,发现,MT7620默认支持24bit(3byte)的spi地址模式, 而要支持32M以上的spi flash,则必须切换到32bit(4byte)地址模式.在soft reset的时候, spi停留在了32bit模式,没有切换回默认的24bit模式,导致reset后,MT7620在默认的24bit模式, 无法和32bit模式的spi通讯,系统死机.那么问题来了:如何在soft reset时刻, 让spi flash切换回24bit模式呢?本文通过设备驱动中的一个shutdown方法来解决这个问题.
然后楼主也提供解决方案了,
首先,清除干净你编译的内核先:make target/linux/{clean,prepare} V=s QUILT=1
然后就可以开始修改内核了,
完整路径如下:~openwrt/build_dir/target-mipsel_24kc_musl-1.1.16/linux-rampis_mt7688/linux-4.4.71/drivers/mtd/devices
然后修改m25p80.c文件static int m25p_remove(struct spi_device *spi) { struct m25p *flash = spi_get_drvdata(spi); // manfeel note: add spi flash reset code flash->command[0] = 0x66; spi_write(flash->spi, flash->command, 1); flash->command[0] = 0x99; spi_write(flash->spi, flash->command, 1); /* Clean up MTD stuff. */ // return mtd_device_unregister(&flash->mtd); 旧版本 return mtd_device_unregister(&flash->mtd); 新版本内核 } static struct spi_driver m25p80_driver = { .driver = { .name = "m25p80", .owner = THIS_MODULE, }, .id_table = m25p_ids, .probe = m25p_probe, .remove = m25p_remove, // manfeel, add shutdown method to reset spi flash .shutdown = m25p_remove, /* REVISIT: many of these chips have deep power-down modes, which * should clearly be entered on suspend() to minimize power use. * And also when they're otherwise idle... */ };
-
RT Thread外设驱动添加-STH3x
2021-01-29 00:43:44以下是关于如何配置SHT3X外设驱动的例程: 1、前提要求 - 熟练使用 ENV 工具 - 熟悉 Kconfig 语法 - 熟悉 STM32CubeMX 工具 - 对 RT-Thread 设备驱动框架有一定了解 2、如何添加更多的外设驱动选项 2、如果是... -
DSP28335外设驱动库
2020-12-22 22:01:38DSP28335外设源码 -
APM飞控添加新的外设驱动
2019-09-14 16:59:56APM飞控添加新的外设驱动,原文链接:http://www.nufeichuiyun.com/?p=300 -
EFM32外设驱动官方示例peripheral_examples.rar
2020-04-05 18:53:25包含EFM32各个系列的外设驱动示例代码。 参考链接:https://github.com/SiliconLabs/peripheral_examples Supported Series 0 Devices EFM32ZG EFM32HG EFM32TG EFM32G EFM32LG EFM32GG EFM32WG Supported Series 1... -
实验四外设驱动程序设计
2017-12-03 23:13:00实验四外设驱动程序设计 学习资源中全课中的“hqyj.嵌入式Linux应用程序开发标准教程.pdf”中的第十一章 设备驱动概述: 对设备驱动最通俗的解释就是“驱使硬件设备行动” 。设备驱动与底层硬件直接打交道, 按照... -
基于zynq平台linux外设驱动分析-内核部分
2015-11-13 10:48:19首先声明文档是自己原创的,通过刻苦阅读linux源代码和翻阅xilinx zynq datasheet写出来的总结文档,对于想了解基于zynq的linux外设驱动的童鞋,有一定帮助,由于是呕心原创,所以分数较高,望见谅 -
20155223 实验四 外设驱动程序设计
2019-09-27 13:51:2720155223 实验四 外设驱动程序设计 实验任务一 学习资源中全课中的“hqyj.嵌入式Linux应用程序开发标准教程.pdf”中的第十一章 提交康奈尔笔记的照片 提交照片: 实验任务二 在Ubuntu完成资源中全课中的“hqyj.... -
第12章 Linux设备驱动的软件架构思想之设备驱动的分层思想(主机驱动与外设驱动分离的设计思想)
2018-05-29 14:07:0012.4 主机驱动与外设驱动分离的设计思想12.4.1 主机驱动与外设驱动分离 Linux中SPI、I2C、USB等子系统都利用典型的把主机驱动和外设驱动分离的想法,主机端只负责产生总线上的传输波形,而外设端通过标准的API让... -
读写卡模块的外设驱动代码编写技巧_python怎么导入自己写的模块
2020-11-07 05:12:27读写卡模块的外设驱动代码编写技巧 6.4 读写卡模块 > 6.4.1 基本功能 2C 2C 输出数据 2C 和 UART 两种通 信方式其引脚含义详见表 6.19 2C 通信时 需要连接 J1 中的电源引脚和 SCLSDA 引 脚由于 I2C 从机不能主动向 I... -
rtthread学习之(2)——STM32 系列外设驱动添加指南
2020-11-22 16:48:44本文档是为需要给现有的 STM32 BSP 添加更多外设驱动的开发者准备的。通过阅读本文,开发者可以按照自己的实际情况给现有 BSP 添加自己需要的驱动。 2. 前提要求 熟练使用 ENV 工具,参考:RT-Thread env 工具用户... -
STM32系列BSP外设驱动使用教程
2020-07-08 09:11:33BSP 外设驱动使用教程 简介_70) 本文档是为需要在 RT-Thread 操作系统上使用更多开发板资源的开发者准备的。通过使用 ENV 工具对 BSP 进行配置,可以开启更多板载资源,实现更多高级功能。 主要包括以下内容: 如何... -
2017-2018-1 20155227 20155318 实验四 外设驱动程序设计
2019-10-02 06:49:012017-2018-1 20155227 20155318 实验四 外设驱动程序设计 实验目的,实验步骤 实验过程如下。 实验四外设驱动程序设计-1 学习资源中全课中的“hqyj.嵌入式Linux应用程序开发标准教程.pdf”中的第十一章 提交康奈尔... -
STM32F4外设驱动系列之GPIO
2019-07-04 20:39:20STM32F4外设驱动系列之GPIOGPIO常用相关寄存器GPIO初始化函数与参数GPIO初始化函数:HAL_GPIO_Init()GPIO初始化参数结构体:GPIO_InitTypeDefGPIO初始化实例 GPIO常用相关寄存器 MODER:GPIO端口模式控制寄存器... -
STM32F4外设驱动系列之IWDG
2019-07-05 10:12:48STM32F4外设驱动系列之IWDGIWDG相关寄存器IWDG初始化函数与参数IWDG相关函数IWDG初始化参数结构体IWDG初始化实例 IWDG相关寄存器 IWDG_KR:关键字寄存器 IWDG_PR:预分频器寄存器 IWDG_RLR:重载寄存器 IWDG_SR:... -
2017-2018-1 20155218 20155205 实验四 外设驱动程序设计
2019-09-23 21:22:522017-2018-1 20155218 20155205 实验四 外设驱动程序设计 实验内容 实验四外设驱动程序设计-1 学习资源中全课中的“hqyj.嵌入式Linux应用程序开发标准教程.pdf”中的第十一章 提交康奈尔笔记的照片(可以多张) ... -
2017-2018-1 20155304 20155332 实验四 外设驱动程序设计
2019-10-02 09:23:32实验四 外设驱动程序设计 实验四外设驱动程序设计-1 学习资源中全课中的“hqyj.嵌入式Linux应用程序开发标准教程.pdf”中的第十一章 提交康奈尔笔记的照片(可以多张) 实验四外设驱动程序设计-2 在Ubuntu完成资源... -
HAL库学习笔记-10 HAL库外设驱动框架概述
2021-04-12 14:13:07HAL库借鉴面向对象的设计思想,将外设驱动封装为对象。 采用此种开发方式有以下特点: 屏蔽底层硬件:只需了解相关接口函数的功能和参数要求即可 提高开发效率:开发难度较小,开发周期较短,后期的维护升级、以及...