精华内容
下载资源
问答
  • 基于PC的虚拟总线驱动开发 实现了 pxl9054硬件DMA驱动 wdm实现 虚拟总线程序
  • 虚拟串口驱动 开发 敏捷的软件开发方法依赖于服务虚拟化,以赋予每个IT团队自治权。 这种方法消除了障碍,使自治团队可以继续开发活动,而不必等待任何人。 这样,团队就可以开始迭代/冲刺,从而开始集成测试。 ...

    虚拟串口驱动 开发

    敏捷的软件开发方法依赖于服务虚拟化,以赋予每个IT团队自治权。 这种方法消除了障碍,使自治团队可以继续开发活动,而不必等待任何人。 这样,团队就可以开始迭代/冲刺,从而开始集成测试。

    自动化服务的工作方式

    消费者可以通过已发布的端点使用任何自动化服务。 这意味着只有在线提供服务后,服务才能自动化。

    任何希望利用可用的自动化服务的消费者都必须能够通过HTTP协议向该服务的端点发送请求。 这些服务中的某些服务在通过HTTP协议接收到请求后,将通过简单地发送一些数据进行响应。 其他服务可能会通过实际执行一些工作来响应通过HTTP协议收到的请求。 例如,服务可以创建资源(例如,创建订单),更新资源(更新订单)或删除资源(取消订单)。

    所有这些活动都是通过HTTP协议触发的。 在最简单的情况下,服务使用者发起的动作是GET(例如HTTP GET)。 该请求可能带有一些查询值。 服务将使用这些值来缩小搜索范围(例如“搜索订单号12345并返回数据”)。

    什么是服务虚拟化?

    现在我们了解了自动化服务的工作原理,应该更容易理解如何对其进行虚拟化。 简而言之,可以模拟托管站点上发布的任何服务。 您可以插入模拟真实服务行为的伪造服务,而不是直接向服务提供商的端点发送HTTP请求。

    从服务使用者的角度来看,无论是与真实服务还是虚假服务进行交互,都没有任何区别。 相互作用保持相同。

    虚拟化一项服务

    好吧,足够多的谈话,我会袖手旁观,并展示如何实际操作。 假设您的团队正在开始一个新项目,并以完整的用户故事形式接收需求:

    验证用户

    作为新应用
    我想验证用户
    因为我们要确保应用程序的适当安全性

    验收标准

    方案1: 新应用成功验证了用户身份
    假设用户已导航到登录页面
    并且用户已提交凭据
    当新应用收到登录请求时
    然后,新应用成功验证了用户身份
    新的应用程序将显示响应消息“用户已成功登录”。

    方案2: 新应用无法在首次尝试时对用户进行身份验证
    假设用户已导航到登录页面
    并且用户已提交凭据
    当新应用收到登录请求时
    然后,新应用无法成功验证用户身份
    新应用会显示响应消息“登录错误。您还有2次尝试失败。”

    方案3: 新应用无法在第二次尝试中对用户进行身份验证
    假设用户已导航到登录页面
    并且用户已提交凭据
    当新应用收到登录请求时
    然后,新应用无法成功验证用户身份
    新应用会显示响应消息“登录不正确。您还有1次尝试机会。”

    方案4: 新应用无法在第三次尝试时对用户进行身份验证
    假设用户已导航到登录页面
    并且用户已提交凭据
    当新应用收到登录请求时
    然后,新应用无法成功验证用户身份
    新的应用程序将显示响应消息“登录错误。您再也没有尝试了。”

    开始研究此用户故事时,要做的第一件事是创建所谓的“行走骨架”(对于本练习,我将使用标准的.Net Core平台以及我在之前的文章中讨论的xUnit.net从这一个开始,再到另一个示例 )。有关如何安装,配置和运行所需工具的技术详细信息,请参考它们。

    通过打开命令行并键入以下内容来创建行走骨架基础结构:

     mkdir AuthenticateUser 
    

    然后移入AuthenticateUser文件夹:

     cd AuthenticateUser 
    

    并创建一个单独的文件夹进行测试:

     mkdir tests 
    

    移入tests文件夹( cd tests )并启动xUnit框架:

     dotnet new xunit 
    

    现在将一个文件夹上移(回到AuthenticateUser )并创建app文件夹:

    mkdir app
    cd app

    创建C#代码所需的支架:

     dotnet new classlib 
    

    现在可以行走的骨架了! 打开您选择的编辑器并开始编码。

    首先编写失败的测试

    本着TDD的精神,从编写失败的测试开始(请参阅上一篇文章,以了解在使测试通过之前,查看失败的原因很重要):

    using System ;
    using Xunit ;
    using app ;

    namespace tests {
        public class UnitTest1 {
            Authenticate auth = new Authenticate ( ) ;

            [ Fact ]
            public void SuccessLogin ( ) {
                var given = "credentials" ;
                var expected = "Successful login." ;
                var actual = auth . Login ( given ) ;
                Assert . Equal ( expected, actual ) ;
            }
        }
    }

    此测试表明,如果有人在处理请求时向Authenticate组件的Login方法提供了一些凭据(即,秘密的用户名和密码),则预期会返回消息“ Successful login”。

    当然,这是尚不存在的功能-SuccessLogin()模块中的实例化Authenticate模块尚未编写。 因此,您不妨继续尝试并首先编写所需的功能。 app文件夹中创建一个新文件( Authenticate.cs ),并添加以下代码:

    using System ;

    namespace app {
        public class Authenticate {
            public string Login ( string credentials ) {
                return "Not implemented" ;
            }
        }
    }

    现在,导航到tests文件夹并运行:

     dotnet test 
    
    Output of dotnet.test

    该测试失败,因为它期望获得“成功登录”输出,但获得了“未实现”输出。

    第二天操作的复杂性增加

    现在您已经创建了“幸福的道路”期望,并使其失败了,现在该着手实现使失败的测试通过的功能。 第二天,您参加站立会议并报告您已开始“认证用户”故事。 您让团队知道您已经为“快乐之路”创建了第一个失败的测试,而今天的计划是实施代码以使失败的测试通过。

    您将说明打算首先创建一个包含用户名密码和其他相关属性的用户表的意图。 但是Scrum 管理员打断了并解释说User模块正在由另一个团队处理。 重复维护用户是不好的做法,因为信息将很快失去同步。 因此,您无需构建User模块(该模块将包括身份验证逻辑),而是利用User团队正在使用的身份验证服务。

    这是个好消息,因为它免除了您必须编写大量代码来实现User处理的麻烦。 底气十足,您热情洋溢地宣布,您将Swift完善一个功能,该功能将获取用户凭据并将其发送给用户团队已建立的服务。

    las,当您得知用户团队尚未开始构建用户身份验证服务时,您的意图再次被压制。 他们仍在为积压工作分配用户故事。 令人沮丧的是,您辞职的事实是,您至少需要几天(如果不是几周?)就可以开始研究用户认证故事。

    然后,Scrum 管理员说没有理由等待构建用户身份验证服务并将其部署到测试中。 您可以立即开始开发身份验证功能。 但是你该怎么做呢?

    Scrum管理员提出了一个简单的建议:利用服务虚拟化。 由于用户模块的所有规范均已固化并签字,因此您将拥有坚实,稳定的合同来构建您的解决方案。 用户服务团队发布的合同规定,为了对用户进行身份验证,必须满足特定的期望:

    1. 希望验证用户身份的客户端应将HTTP POST请求发送到端点http://some-domain.com/api/v1/users/login
    2. 发送到上述端点的HTTP POST必须具有包含用户凭证(即用户名和密码)的JSON有效负载。
    3. 收到请求后,服务将尝试登录用户。如果用户名和密码与记录的信息匹配,则该服务将返回一个HTTP响应,其中包含状态码200,响应的正文包含消息“用户已成功登录”。在。”

    因此,既然您知道合同的详细信息,就可以开始构建解决方案了。 这是连接到端点,发送HTTP POST请求并接收HTTP响应的代码:

    using System ;
    using System. Net . Http ;
    using System. Threading . Tasks ;
    using System. Collections . Generic ;

    namespace app {
        public class Authenticate {
            HttpClient client = new HttpClient ( ) ;
            string endPoint = "http://some-domain.com/api/v1/users/login" ;

            public string Login ( string credentials ) {
                Task < string > response = CheckLogin ( credentials ) ;
                return response . Result ;
            }

            private async Task < string > CheckLogin ( string credentials ) {
                var values = new Dictionary < stringstring > { { "credentials" , credentials } } ;
                var content = new FormUrlEncodedContent ( values ) ;
                var response = await client . PostAsync ( endPoint, content ) ;
                return await response . Content . ReadAsStringAsync ( ) ;
            }
        }
    }

    由于http://some-domain.com不存在(尚未),因此该代码不起作用。 您现在停滞不前,等待其他团队最终构建和部署该服务吗?

    并不是的。 服务虚拟化抢救! 让我们假装该服务已经存在并继续开发。

    如何虚拟化服务

    虚拟化用户身份验证服务的一种方法是编写一个新应用(新API)并在本地运行。 该API将镜像实际用户身份验证 API指定的合同,并且仅返回硬编码的存根数据(它将是伪造的服务)。

    听起来是个好计划。 再次,团队在站立时退缩,质疑是否需要编写,构建,测试和部署全新的应用程序才能完成此虚假功能。 这种麻烦根本不值得,因为当您交付新的假应用时,其他团队可能已经准备好提供真正的服务。

    所以你陷入了僵局。 看来您被迫等待依赖的实现。 您无法控制依赖关系; 您现在没有其他资源,只能按顺序工作。

    没那么快! 有一个名为mountebank的出色新工具,非常适合虚拟化任何服务。 使用此工具,您可以快速站起本地服务器,该服务器侦听您指定的端口并接受订单。 要使其模拟服务,您只需告诉它要侦听的端口以及要处理的协议。 协议的选择是:

    • HTTP
    • HTTPS
    • SMTP
    • TCP协议

    在这种情况下,您需要HTTP协议。 首先,安装mountebank-如果您的计算机上装有npm ,则只需在命令行中键入:

     npm install -g mountebank 
    

    安装后,通过键入以下内容运行mountebank:

     mb 
    

    在启动时,mountebank将显示:

    mountebank startup

    现在您可以虚拟化HTTP服务了。 在这种情况下, 用户身份验证服务希望收到HTTP POST请求; 这是实现的代码如何发送HTTP POST请求的方法:

     var response = await client.PostAsync ( endPoint , content ) ; 
    

    现在,您必须建立该endPoint 理想情况下,所有虚拟化服务都应在localhost服务器中进行支持,以确保快速执行集成测试。

    为此,您需要配置imposter 冒名顶替者是一个简单的JSON集合,包含一个端口和一个协议的定义:

    {
        "port" : 3001 ,
        "protocol" : "http"
    }

    该冒名顶替者被配置为处理HTTP协议,并侦听端口3001上的传入请求。

    仅侦听端口3001上的传入HTTP请求不会做很多事情。 一旦请求到达该端口,就需要告知mountebank如何处理该请求。 换句话说,您不仅要虚拟化特定端口上服务的可用性,还要虚拟化虚拟化服务响应请求的方式。

    要实现该级别的服务虚拟化,您需要告诉mountebank如何配置存根。 每个存根包含两个组件:

    1. 谓词集合
    2. 预期回应的集合

    谓词(有时称为匹配器)缩小了传入请求的范围。 例如,使用HTTP协议,可以期望不止一种类型的方法(例如GET,POST,PUT,DELETE,PATCH等)。 在大多数服务虚拟化方案中,我们有兴趣模拟特定于特定HTTP方法的行为。 这种情况是关于响应HTTP POST请求的,因此您需要将存根配置为仅与HTTP POST请求匹配:

    {
        "port" : 3001 ,
        "protocol" : "http" ,
        "stubs" : [
            {
                "predicates" : [
                    {
                        "equals" : {
                            "method" : "post"
                        }
                    }
                ]
            }
        ]
    }

    该冒名顶替者定义了一个仅与HTTP POST请求匹配(使用关键字equals )的谓词。

    现在,仔细研究一下已实现的代码中定义的endPoint值:

     string endPoint = "http://localhost:3001/api/v1/users/login" ; 
    

    除了侦听端口3001(在http:// localhost:3001中定义)外, endPoint更具体,因为它希望传入的HTTP POST请求进入/ api / v1 / users / login路径。 您如何告诉mountebank仅在/ api / v1 / users / login路径上完全匹配? 通过将路径键值对添加到存根的谓词中:

    {
        "port" : 3001 ,
        "protocol" : "http" ,
        "stubs" : [
            {
                "predicates" : [
                    {
                        "equals" : {
                            "method" : "post" ,
                            "path" : "/api/v1/users/login"
                        }
                    }
                ]
            }
        ]
    }

    现在,这个冒名顶替者知道到达端口3001的HTTP请求必须是POST方法,并且必须指向/ api / v1 / users / login路径。 剩下要做的唯一模拟是预期的HTTP响应。

    将响应添加到JSON冒名顶替者:

    {
        "port" : 3001 ,
        "protocol" : "http" ,
        "stubs" : [
            {
                "predicates" : [
                    {
                        "equals" : {
                            "method" : "post" ,
                            "path" : "/api/v1/users/login"
                        }
                    }
                ] ,
                "responses" : [
                    {
                        "is" : {
                            "statusCode" : 200 ,
                            "body" : "Successful login."
                        }
                    }
                ]
            }
        ]
    }

    使用mountebank冒名顶替者,您可以将响应定义为JSON键值对的集合。 在大多数情况下,只需声明响应是statusCodebody即可 这种情况正在模拟状态代码为OK(200)且包含简单消息“ 成功登录” (如接受标准中所指定的正文的“快乐路径”响应。

    如何运行虚拟化服务?

    好的,既然您已经虚拟化了用户身份验证服务(至少是它的“快乐之路”),如何运行它?

    请记住,您已经启动了mountebank,它报告说它正在内存中作为http:// localhost域运行。 Mountebank正在监听2525端口并接受订单。

    太好了,现在您必须告诉Mountebank您已经准备好冒名顶替者。 你是怎样做的? 将HTTP POST请求发送到http:// localhost:2525 / imposters 请求正文必须包含您在上面创建的JSON。 有几种技术可以发送该请求。 如果您精通curl ,则使用它发送HTTP POST请求将是抵御冒名顶替者的最简单,最快的方法。 但是许多人喜欢一种更加用户友好的方式来将HTTP POST发送到mountebank。

    最简单的方法是使用Postman 如果您下载并安装Postman,则可以将其指向http:// localhost:2525 / imposters ,从下拉菜单中选择POST方法,然后将imposter JSON复制并粘贴到原始正文中。

    单击发送时,冒名顶替者将被创建,并且您应获得状态201(已创建)。

    Postman output

    您的虚拟化服务正在运行! 您可以通过导航到tests文件夹并运行dotnet test命令来验证它:

    dotnet test output

    结论

    该演示演示了通过模拟您依赖的服务来消除阻塞和控制依赖关系是多么容易。 Mountebank是一款出色的工具,可轻松,廉价地模拟各种非常精细,复杂的服务。

    在这一期中,我只是有时间来说明如何虚拟化简单的“幸福之路”服务。 如果返回到实际的用户案例,您会注意到它的接受条件包含几个“不太满意”的路径(某些情况下,当有人反复尝试使用无效的凭据登录时)。 适当地虚拟化和测试这些用例会有些棘手,因此我将该练习留给了本系列的下一部分。

    您将如何使用服务虚拟化解决您的测试需求? 我希望在评论中听到它。

    翻译自: https://opensource.com/article/20/3/service-virtualization-test-driven-development

    虚拟串口驱动 开发

    展开全文
  • by fanxiushu 2020-03-25 转载或引用请注明原始作者。...以前的文章阐述过基于windows平台和基于linux平台中的USB虚拟总线驱动开发, 比如如下链接阐述的是在linux平台中的虚拟USB总线驱动开发原理:https://blo...

                                    by fanxiushu 2020-03-25 转载或引用请注明原始作者。

    USB虚拟总线驱动的使用范围是非常广泛的,可以使用它来模拟各种通用的USB设备。
    以前的文章阐述过基于windows平台和基于linux平台中的USB虚拟总线驱动开发,
    比如如下链接阐述的是在linux平台中的虚拟USB总线驱动开发原理:
    https://blog.csdn.net/fanxiushu/article/details/102967402
    稍微再早一点的文章阐述了windows平台中的虚拟USB总线驱动开发,但是windows中的虚拟USB总线驱动实现起来比起linux复杂得多。

    到目前为止,使用虚拟USB总线驱动模拟了虚拟摄像头,虚拟声卡和虚拟麦克风,虚拟鼠标键盘,虚拟触摸屏,。。。
    好像日常用到的都模拟遍了,不对,U盘还没模拟。
    因此这篇文章就是阐述模拟U盘的具体通讯协议过程。

    绝大部分U盘通讯协议都是基于SCSI通讯的,也就是在底层的USB通讯协议中封装了SCSI协议来实现U盘的功能。

    正如上一篇文章在阐述windows平台实现基于AVStream框架的虚拟摄像头的时候所说的一样。
    如果你的需求只是实现一个即插即用的移动硬盘,
    不应该采用这种通过虚拟USB总线驱动模拟U盘的办法,因为同样的原因:代价是高昂的,过程是冗余的。
    因此本文介绍的内容研究意义可能大于实际使用价值,不过对掌握硬件U盘实际通讯过程也有一定参考意义。
    当然这里还有一个好处就是基于USB通讯封装的SCSI协议是跨平台的。
    因此本尽量阐述协议部分,不与具体的(比如windows或linux)虚拟USB总线驱动联系起来。

    实际上,在windows这样的系统中,可以直接使用Storport 这样的磁盘驱动框架来实现虚拟磁盘。
    而且基本上几百行代码就能实现一个基于 Storport的虚拟磁盘驱动框架。
    具体原理可以查看我很早前发布的文章,如下所示,
    同时本文介绍的利用USB总线驱动模拟U盘,使用的SCSI通讯协议和下面链接中使用的SCSI基本上是一样的。
    https://blog.csdn.net/fanxiushu/article/details/9903123  (磁盘驱动与虚拟磁盘MINIPORT驱动一)
    https://blog.csdn.net/fanxiushu/article/details/11713357  (磁盘驱动与虚拟磁盘MINIPORT驱动二)
    上面的文章,尤其是第二篇介绍的SCSI命令,在本文中会同样使用到。

    要成功模拟U盘,当然第一步肯定是正确模拟出USB的设备描述符,配置描述符,接口描述符,端口描述符。
    U盘的这些描述符其实是挺简单的。我们可以直接把某个现成的U盘的描述符copy过来,直接使用。
    绝大部分U盘都是基于 Bulk-Only 方式来传输数据的,这种方式简单而且容易理解。
    也就是主机用 ”控制传输方式“ 从U盘获取各种描述符等基本信息之后,
    之后所有基于SCSI的命令,都是封装到 ”Bulk传输“ 中进行通讯的。
    BULK传输需要有两个方向:从U盘传输到主机,从主机传输到U盘,需要两个BULK端点。
    (为了下文引用的方便,这里把从U盘到主机端点定义成 Bulk-In, 把从主机到U盘的端点定义成 Bulk-Out)
    因此U盘通常是包含设备描述符,一个配置描述符,配置描述符中包含一个接口描述符,接口描述符中包含两个端点描述符。

    接着就是如何在Bulk传输中通讯SCSI协议命令。
    我们知道,USB通讯都是主机主动发起USB通讯,USB设备响应命令的主-从方式。
    因此第一个数据包数由主机首先发起,使用 Bulk-Out端点传输,是一个CBW头(Command Block Wraper),
    这个头用于指示接下来需要传输多少传输多少数据,数据传输方向,SCSI的CBD头信息等,如下定义:
    struct usb_cbw_t
    {
        __u32  sig; /// fixd  'USBC'
        __u32  tag; ///
        __u32  data_transfer_length;
        __u8   dir; ///方向 0x80 从设备到主机,  0x00 host -> device
        __u8   lun;   ///
        __u8   cb_length;
        __u8   cb_data[16];
    };
    总共31个字节。
    其中 sig固定为 0x43425355(‘USBC’),tag是主机随机生成的一个数字,用于在回复CSW的时候使用。
    data_transfer_length 就是表示接下来需要传输的数据大小,如果为0,表示就只传输CBW头,不传输数据。
    dir是接下来的数据传输方向,
    如果0x80表示从设备传输到主机,这个时候使用Bulk-IN端点传输。如果0 表示从主机传输到设备,使用Bulk-Out传输。
    lun表示 SCSI磁盘设备的逻辑位置,一般一个U盘就设置一个SCSI磁盘,因此通常都是 0,
    cb_data 就是SCSI通讯定义的CDB头,不超过16字节,具体大小由cb_length指定。
    我们可以通过CDB头,指定发起了哪个SCSI命令。

    主机通过BULK-Out发起了一个CBW包之后,接下来,如果data_transfer_length 大于0, 则开始传输实际的数据。
    然后根据CBW中的dir参数判断方向, 从而判断是采用 Bulk-In,还是Bulk-Out传输。
    如果指定的data_transfer_length长度数据传输完成,U盘会通过 Bulk-IN端点,回复主机一个CSW包。
    如果传输过程中,U盘出现故障等问题,则直接回复 STALL的USB通讯错误。
    如果data_transfer_length长度为0,则不需要传数据,但是U盘同样需要通过 Bulk-In端点回复CSW数据包。
    CSW(Command Status Wrapper)数据包定义如下:
    struct usb_csw_t
    {
        __u32  sig;  /// 'USBS'
        __u32  tag; 
        __u32  data_rest;  ///还剩下多少字节需要传输
        __u8   status;     0 success, 1 error
    };
    一共13个字节。
    其中sig固定为0x53425355(‘USBS’),tag跟主机发起的CWB包中的tag保持一致。
    data_reset表示回复的时候,还需要多少数据需要传输。
    status表示本次SCSI传输是成功,还是失败, 0 表示成功,非0表示失败。

    总结一下,通过Bulk-In和Bulk-Out两个端点,完成一个SCSI命令的传输过程:
    1,主机  发送 31个字节的 CBW 头(通过 Bulk-Out)
    2,传输数据(如果有的话),(通过Bulk-In或Bulk-Out,具体根本CBW中的参数决定。)
    3,U盘回复13个字节的CSW包,(通过Bulk-In)

    以上传输需要严格按照顺序进行,如果出现错乱,通常主机就会发起 reset device 的USB命令。

    接下来,拨开USB通讯部分,分析SCSI命令。
    SCSI命令是非常多的,好在我们实现U盘,其实只需关心其中几个比较关键的命令。
    SCSI的CDB头的第一个字节表示的就是当前命令类型,
    比如 Inquiry 是SCSIOP_INQUIRY(0x12),这个是主机获取SCSI设备的基本信息,U盘需要回复一个INQUIRYDATA结构。
    结构描述可直接查询 WDK驱动的 storport.h 头文件的描述。
    因为SCSI命令是跨平台,所以在WDK中描述的这些结构同样适合于 linux 这样的平台。
    通常实现一个U盘需要使用到的SCSI命令如下:
    SCSIOP_INQUIRY                      扫描磁盘
    SCSIOP_READ_CAPACITY       获得磁盘容量
    SCSIOP_READ                          读磁盘
    SCSIOP_WRITE                         写磁盘
    SCSIOP_MODE_SENSE             获得磁盘相关参数

    SCSIOP_TEST_UNIT_READY
    SCSIOP_SYNCHRONIZE_CACHE
    SCSIOP_START_STOP_UNIT
    SCSIOP_VERIFY           以上4个命令跟首次使用磁盘时候,检查磁盘单元有关。
    这个与在
    https://blog.csdn.net/fanxiushu/article/details/11713357  (磁盘驱动与虚拟磁盘MINIPORT驱动二)
    中描述的基本一致,因此要了解详细信息,可去查阅如上链接的文章。

    下图是利用前段时间开发的基于linux平台的虚拟USB总线驱动,模拟出来的一个U盘。
    估计是大家对windows平台都烂熟了,所以来个比较新奇的linux平台下的模拟效果图。





     

    展开全文
  • windows虚拟网卡驱动开发

    千次阅读 热门讨论 2017-04-06 19:05:24
    by fanxiushu 2017-04-06 转载或...很早前的文章介绍过windows和linux平台的虚拟网卡技术, 详见 http://blog.csdn.net/fanxiushu/article/details/8526708 http://blog.csdn.net/fanxiushu/article/details/8526
     
                                                                                                                              by fanxiushu   2017-04-06 转载或引用请注明原始作者。

    很早前的文章介绍过windows和linux平台的虚拟网卡技术,
    详见
         http://blog.csdn.net/fanxiushu/article/details/8526708
         http://blog.csdn.net/fanxiushu/article/details/8526719
         http://blog.csdn.net/fanxiushu/article/details/8525749
         http://blog.csdn.net/fanxiushu/article/details/8507638
    前两个是讲述如何组成一个虚拟局域网,后边的是如何在linux平台下开发一个虚拟网卡
    (当时提供的代码比较老,需要修改才能在新版本linux下使用,
    或者懒得自己开发,直接使用linux自带的tun驱动,linux底层这些驱动总比windows平台方便得多)。
    这些文章介绍过如何利用虚拟网卡组建局域网的原理:获取应用层程序发给虚拟网卡的数据包,
    然后通过真实的网络发给服务端, 服务端再转发给另外一台机器,这台机器再把从网络获取的数据包传递给虚拟网卡。
    通过这样的方式,就把处于不同真实网络环境中的机器连接到同一个虚拟局域网中。
    只是当时没介绍如何开发windows虚拟网卡驱动,这篇文章填补这个空白。

    win7系统有最新的NDIS6.2框架,win8 的NDIS提高到6.3以上,win10 达到ndis6.4 。
    最大变化是从NDIS5.x 到 NDIS6.x, 连最基本的包的定义等数据结构都发生了巨大变化。
    但是windows有个最大优点,就是兼容,在win7,win8,win10,等平台可以运行ndis5.x框架的驱动,
    (不过ndis5.x的中间驱动无法在win10上运行,这个估计是最大不方便了)
    就跟TDI驱动能在各种windows平台通吃一样,NDIS5.x也能通吃各种windows平台。
    这里采用 NDIS5.1框架,不是要抱着老的框架不放,而是许多用户抱着WinXP 不放,
    同时要兼容 WinXP和WIN7,而且也不用开发两套代码的最好选择就是NDIS5.1 了。
    如果你的程序只运行在WIN7系统以上,可以只使用NDIS6以上的版本的框架,
    NDIS6虽然基本结构尤其是包结构改变了,但是我们开发的总体方式差不多。

    首先在DriverEntry中声明 NDIS_MINIPORT_CHARACTERISTICS 变量,它是一个包含多个回调函数的数据结构,
    在此结构中填写好我们需要的各种回调函数之后,调用 NdisMRegisterMiniport 函数注册。

    NdisMRegisterMiniport虽然没开放源代码,但是基本工作流程应该能想到,因为虚拟网卡驱动也是即插即用驱动模型,
    因此在DriverEntry 函数中一样需要实现 AddDevice,以及各种派遣函数,
    只是 NdisMRegisterMiniport  使用它内部的某个函数 设置到AddDevice 回调中,同时设置各种IRP_MJ_XXX派遣函数,
    并且做一些其他初始化操作,当有设备(也就是网卡)插上来,DriverObject->DriverExtension->AddDevice 函数被调用,
    这时会进入到NdisMRegisterMiniport注册的 某个内部函数中,
    在这个函数中会调用 NDIS_MINIPORT_CHARACTERISTICS  导出的 InitializeHandler 函数,
    这样就进入到我们注册的网卡初始化函数。

    在虚拟网卡驱动中,主要实现以下几个回调函数,基本上就能完成一个虚拟网卡的功能:

      InitializeHandler , 初始化网卡。也就是当我们安装一块网卡实例驱动的时候,这个函数被调用,
                                      在这个函数中,初始化各种资源,这个函数等同于普通的即插即用驱动的AddDevice函数,
                                      只是被NDIS框架封装成 InitializeHandler 回调函数了。
     HaltHandler , 卸载网卡,当我们卸载某个网卡驱动时候,这个函数被调用,
                             相当于普通即插即用驱动程序收到 IRP_MN_REMOVE_DEVICE等消息之后触发的回调。

    QueryInformationHandler, 查询网卡 OID。其实就是查询网卡的各种信息,网卡包含的信息很多,基本上有几十个。
    SetInformationHandler,      设置网卡OID。 设置我们感兴趣的OID信息。
    ResetHandler, 是否重启网卡,虚拟网卡驱动中,基本用不上。
    CheckForHangHandler, 检测网卡是否处于hang状态,是的话,调用ResetHandler, 虚拟网卡基本上也用不着。

    SendPacketsHandler, 处理网络数据包的核心函数之一,这个回调函数表示网卡从应用层程序接收到以太网数据包,
                                          比如在应用层调用套接字函数send 或sendto发送数据,数据进入到内核的传输层,
                                          经过分析剥离,进入到NDIS协议驱动层,协议驱动层找到这个数据是朝哪个网卡发送的,
                                          于是找到这个网卡注册的  SendPacketsHandler 回调函数地址,
                                          最后调用这个回调函数实现数据包的真正发送。                             
                                          在SendPacketsHandler  函数中处理的数据包是准备发给底层的物理链路的,
                                          虚拟网卡没有物理链路,因此我们把这些数据包入队,
                                          然后直接在驱动层通过WSK(TDI)方式(或者其他各种方式,如USB, 串口等)发给远端设备或电脑,
                                          或者把数据包传递到应用层, 让我们的应用层程序做各种处理,为了开发的方便和简洁,
                                          我们采用的是传递到应用层来处理。

    ReturnPacketHandler, 这个函数与上边的刚好相反,当物理链路有数据包到达(或者通过其他方式有数据包,如USB等),
                                          调用NDIS函数NdisMIndicateReceivePacket,通知上层有个数据包达到,
                                          等上层(这个上层就是处理TCP/IP等各种协议的协议层)处理完这个数据包之后,
                                          ReturnPacketHandler 就被调用。
                                          接着这个数据被上传到传输层进一步分析处理,
                                          再进入到应用层,这时候调用 recv或者recvfrom等套接字函数的程序就接收到了数据。
                                          我们的虚拟网卡驱动在应用层程序通过某个IOCTL控制命令传递一个数据包到驱动,
                                          在驱动中直接调用NdisMIndicateReceivePacket通知上层有数据包到达。

    CancelSendPacketsHandler, 这个是NDIS5.1框架中,提供的取消某些数据包发送的回调函数,也就是上层调用SendPacketsHandler,
                                         发送数据包,但是我们的驱动还没来得及处理,只是入队等待处理,这个时候上层决定取消某些数据包的发送,
                                         于是调用 CancelSendPacketsHandler 让我们取消某些数据包的发送。
    PnPEventNotifyHandler, NDIS5.1框架的PnP通知事件,其实就是对应普通的即插即用驱动中的IRP_MJ_PNP请求的封装。
    AdapterShutdownHandler, NDIS5.1框架的网卡关闭事件。

    因为我们的虚拟网卡驱动是把数据包传递到应用层来处理,也就是应用层相当于是“物理连线”,
    必须创建一个控制设备才能跟应用层交换数据,NDIS5.1框架提供了NdisMRegisterDevice 函数来创建一个控制设备,
    在 InitializeHandler 网卡实例初始化函数中可以调用这个函数创建控制设备,
    在 HaltHandler 网卡卸载函数中可以调用NdisMDeregisterDevice删除这个设备。
    创建这个控制设备时候,传递一些参数,包括派遣函数,我们感兴趣的主要是IRP_MJ_DEVICE_CONTROL,以及CREATE /CLOSE 。
    可以定义两个IOCTL命令,一个用于数据包读取,一个用于向驱动写数据包,比如命名为 READ IOCTL 和WRITE IOCTL。
    网卡处理的数据包是非常多的,
    以100M以太网来计算,以太网数据包大小 1514,当全速传输时候, 100M*1024*1024/8/1514 =  大约 8千多个数据包, 
    全速传输,每秒传输8千多个数据包甚至更多, 这个量是很大的,千兆网达到8万多个包每秒,甚至更多。
    为了尽量提高IO吞吐率,在应用层可以采用完成端口方式接收数据包,下边会讲到。
    (开发虚拟网卡驱动相对而言不算太难,难得是如何调优,让他的IO性能提升到更好的效果)

    如此之多的数据包要在应用层和驱动层进行交换,驱动里边该采用什么结构来处理,才能尽可能的提高IO效率呢?
    这里顺带提一下我测试IO效率的方式,
    一个服务端程序,负责多个虚拟网卡数据包转发,它让多个虚拟网卡组成一个虚拟局域网,
    虚拟网卡的客户端程序采用TCP连接到服务端,转发的虚拟网卡数据包也是通过TCP方式转发。
    服务端程序放到一台性能还可以的WIN7的机器上(四代 i7 的 8核CPU,16G内存),
    它的千兆网卡接到千兆交换机上,完全保证它达到千兆网卡的速度。
    一台装有XP系统的古老机器,以前的ATOM的CPU,1G内存,它的网卡接到百兆交换机上。
    一台装WIN10系统的古老CPU是Core2的机器,2G内存,它的网卡接到百兆交换机上。
    测试以TCP传输为主,UDP基本不考虑,
    测试时候使用FTP,HTTP,windows文件夹共享三种方式上传或者下载一个超过1G大小的大文件。
    在100M网络环境下,以上的测试,XP和WIN10之间通过虚拟网络传输文件,
    基本达到5M-6MB的速度,也就是达到50-%60%的真实网卡利用率,
    也许使用更好的机器效果会更高,在装服务端程序的机器上也装上虚拟网卡,让他与WIN10传输文件,
    这个速度就比较高了,维持在7-11M的速度,基本上达到 70-95%的真实网卡利用率,峰值时候基本能达到饱和。
    这种测试还跟客户端服务端程序,机器配置等多种因素有关,因此不能保证换个环境就一定准确。
    整体来看,WIN7以上的系统性能的表现比WINXP系统好,这应该是WIN7上的内核,微软把整个网络层重构了一遍,跟WINXP完全不同。

    我是基于以上测试来评定开发的虚拟网卡的IO性能。
    为此想了许多办法,也做了多种尝试,往往写好了一种方式的代码发现效果不太理想,之后想到另外一种处理方式可能会更好,
    因此废弃先前的代码,重新再实现新想到的处理方式,来来回回的折腾了多种处理方式,采用了如下的处理结构:
    (不过任然是采用单帧接收和发送的方式)
    数据包传递以IRP 请求为主,一个IRP传递一个数据包。也就是上边所说的定义两个IOCTL命令,read ioctl和write ioctl,
    每次都发送和接收一个数据包,都会产生一个IOCTL调用。

    以read ioctl为例,
    应用层投递read ioctl请求到驱动,这个请求的 Irp 都被驱动挂载到某个 IRP队列 比如 tx_irps_head,
    上层数据包发到虚拟网卡驱动 ,也就是 SendPacketsHandler 函数被调用时候,
    把这些数据包挂载到 某个Packet队列,比如 tx_pkts_head 。
    两者每次处理完后,都调用 KeInsertQueueDpc触发DPC调用 ,在DPC执行函数中,检查两个队列是否都不为空,
    都不为空的话,分别取出一个IRP和一个Packet,把Packet数据copy到IRP,完成这个IRP和Packet,如此循环,直到某个队列为空。

    可能有人会问,这里为何要多次一举使用DPC调用,而不是直接执行这种检查处理!?
    一般是在硬件的中断函数中,为了尽快让出中断函数,让稍微耗时的处理交给次一级的函数处理,
    但是也是要求尽快处理完成而不被其他软中断打断,DPC就处于这种地位。
    对软件来说,是很高的运行级别,在DISPACH_LEVEL,运行的时候是不会被调度到其他CPU或者被软中断打断的运行级别。
    对于网络数据包的处理就是需要在这种DPC环境中运行,才能让他达到更好的IO效率。

     大致伪代码如下,
    在IRP_MJ_DEVICE_CONTROL请求中,响应 IOCTL_NCARD_READ_DATA(也就是read IOCTL宏)

    case IOCTL_NCARD_READ_DATA:
             // PENDING
             status = STATUS_PENDING;
             IoMarkIrpPending(Irp);

              InitializeListHead(&Irp->Tail.Overlay.ListEntry);
              Irp->Tail.Overlay.DriverContext[0] = a;
               IoSetCancelRoutine(Irp, ioctl_cacnel_routine); ///
             if (Irp->Cancel) {
                if (IoSetCancelRoutine(Irp, NULL) != NULL) { //取消例程还没被执行,自己取消
                   status = STATUS_CANCELLED;
                   complete_irp(Irp, status, 0);
                }
               
             }
            else {
                ///加入到队列 , 等待 上层有数据包发送 SendPacketsHandler 被调用,再从 tx_irps_head队列取出IRP进行处理。  
                NdisInterlockedInsertTailList(&a->tx_irps_head, &Irp->Tail.Overlay.ListEntry, &a->tx_spinlock);
            }
           
            // 触发DPC调用,这里采用DPC。
            KeInsertQueueDpc(&(a)->tx_dpc, NULL, NULL); /
           .......
         上边的DPC的初始化操作,
            KeInitializeDpc(&a->tx_dpc, adapter_complete_send_packets_dpc, a);
     
          在 adapter_complete_send_packets_dpc 这个DPC执行函数中完成类似如下的操作:
       
    ///--------------------------------------------------------------------------------------------------
        adapter_t* a = (adapter_t*)context;
        adapter_inc(a);
        PLIST_ENTRY entry;
       
        while (TRUE) {
           
            tx_lock(a);

            if (IsListEmpty(&a->tx_irps_head) || IsListEmpty(&a->tx_pkts_head)) {
                tx_unlock(a);
                break;
            }
            entry = RemoveHeadList(&a->tx_irps_head);
            PIRP Irp = CONTAINING_RECORD(entry, IRP, Tail.Overlay.ListEntry );

            if (IoSetCancelRoutine(Irp, NULL) == NULL) { /// 应该检查返回值,若为空,说明取消例程已经被调用了
                NdisInitializeListHead(&Irp->Tail.Overlay.ListEntry); //初始化,防止在取消例程出问题
                tx_unlock(a);
                continue;
            }
           
            entry = RemoveHeadList(&a->tx_pkts_head);
            PNDIS_PACKET packet = CONTAINING_RECORD(entry, NDIS_PACKET, MiniportReserved);

            tx_unlock(a);
            /
            NTSTATUS status = adapter_send_packet_to_irp(a, packet, Irp); 复制 Packet 数据到 Irp

            ///
        }

        adapter_dec(a);
         /------------------------------------------------------------------------------------------------------------

        在网卡 SendPacketsHandler 函数中做类似如下处理:
         
            for (index = 0; index < NumberOfPackets; ++index) {
                  PNDIS_PACKET packet = PacketArray[index];  ///
                 
                  status = NDIS_STATUS_PENDING;
                  NDIS_SET_PACKET_STATUS(packet, status);

                   tx_inc(a);  //增加send包计数
                   NdisInterlockedInsertTailList(&a->tx_pkts_head, (PLIST_ENTRY)packet->MiniportReserved, &a->tx_spinlock); ///挂载到队列

                  ///
             }
             .........
            触发 DPC调用        
            KeInsertQueueDpc(&(a)->tx_dpc, NULL, NULL);
            -------.........................................................

         如上就完成一个数据包的交换。
         write ioctl也做类似的处理。

         回到应用层来,在应用层,一般做法都是阻塞调用 DeviceIoControl ,这样没什么问题。
         但是可以这样考虑,我们在调用DeviceIoControl返回后,处理数据包,然后再接着调用DeviceIoControl,
         在再次调用DeviceIocontrol之前这中间有空隙,如果一次多投递读请求,这样数据包一来就被接收,中间就不存在空隙了。
         最容易想到的就是多线程调用 DeviceIoControl ,但是这种密集型的IO,多线程反而会降低效率,
         而且多线程还容易造成接收到的数据包乱序,这对TCP这种君子协议来说不是好事。
         在一个线程里,同时投递多个请求,异步方式处理,这才是解决这个问题的办法。
         完成端口就可以完成这件事,
         大家所熟悉的完成端口,多用到网络编程上,其实网络套接字只是它的一个应用而已,
         凡是具备异步读写的OVERLAPPED重叠请求的,都可以关联到完成端口。
         这里也就不具体描述如何使用完成端口来异步投递read ioctl请求了,因为相信大家已经很熟悉。
        
         以上开发的虚拟网卡测试的都是在100M网络环境下进行的,但是在千兆网络环境下的测试是很糟糕的。
         我按照上边的环境测试,1000Mbps 的环境下,最快只能达到 20 MBytes 每秒的速度,也就是相当于 千兆网的 五分之一的速度。
         这个测试数据非常让人气馁。
        
         归根结底还是因为每个数据包长度不超过 1514,千兆网每秒需要处理 8多万甚至更多数据包,这么多数据包,
         按照每个包的取,而且取出来之后,再把每个包在应用层封装一下再发到服务端,再服务端再转发出去,
         达到对方虚拟网卡,再传递到网卡驱动层接收,再等待ACK回应,
         这中间的延时比真正的物理硬件环境要高得多了,
         解决这个问题也不是没办法,既然包太多了,那就减少包数量。如果在纯粹的虚拟网络中,可以设置虚拟网卡的MTU值,
         让他更大,比如设置4M这么大的MTU值,这样每个包就可以达到 4M,传递的包个数大大减少,这种情况下疯狂传输文件的话,
         立马就能把千兆网络跑满。
         但是如果把虚拟网卡和真实的网卡混合桥接,1514的包大小的限制是无法改变的,任然要面对大量的包造成的效率问题。
         
          在写这篇文章时候,想到另外一个把数据包传递到应用层的办法,不采用IRP传递。
          就是在应用层开辟一块很大的内存,比如2M的内存,这块内存映射到驱动,这个内存块按照 1514 拆分成起码1000多个小块。
          这些小块组成循环队列, 驱动不停的朝这个循环队列写数据包,应用层不停的从这个循环队列读数据包。
          这样就减少IRP调用的开销,应该能提高效率,但在千兆网环境中能否得到质的提升,因为没实现,所以不能下定论。
          不过即使提升了驱动和应用层的IO效率,但是应用层还得发数据包到网络去转发,这个无论如何也得不到实质的提升。
          总体下来估计不会有质的提升。
         
                                                                                                                                 范秀树 2017-04-06  晚
     

    展开全文
  • 本章即是pinctrl子系统分析的最后一章,本章我们主要实现一个虚拟的pinctrl device驱动,以便我们能够使用pinctrl子系统提供的接口,实现pinctrl device的驱动开发(本章实现的驱动代码可以在ubuntu18.04系统上正常...

    这周主要对pinctrl子系统进行分析,该分析的基本上已经分析完成,唯一没有细说的估计就是gpio与pinctrl之间的关联了。本章即是pinctrl子系统分析的最后一章,本章我们主要实现一个虚拟的pinctrl device驱动,以便我们能够使用pinctrl子系统提供的接口,实现pinctrl device的驱动开发(本章实现的驱动代码可以在ubuntu18.04系统上正常运行)。

    本篇文章的目的如下:

    1. 实现一个虚拟的pinctrl dev驱动,掌握pinctrl dev的驱动开发;
    2. 不需要借助开发板,即可完成pinctrl dev驱动开发及验证工作(我们既然分析内核各驱动子系统模块,要学习的就是他们的系统设计方法、硬件抽象等工作。本篇文章保证在没有硬件开发板的情况下,也可以进行pinctrl 子系统的驱动开发,其实大多数驱动工程师可能都不一定有开发pinctrl 子系统驱动的场景,pinctrl device驱动开发基本上是soc厂家实现的)。

    本篇文章涉及的知识点:

    1. 需要知道platform device、driver的知识;
    2. 需要对sysfs有个大概的理解,我们通过sysfs子系统的属性文件,查看pin mux配置是否生效;
    3. 需要使用一个虚拟的gpio控制器驱动(在之前gpio专栏中已经实现,此处增加对pinctrl的支持),验证gpio相关的引脚配置功能;
    4. 需要使用一个虚拟控制器驱动,验证device与pinctr的绑定功能(此处我们使用之前在spi专栏中实现的虚拟spi控制器驱动,该驱动在此处基本上无需修改)。

    本章的主要章节如下:

    一、 virt soc pin描述

    二、 virt pinctrl dev驱动实现

    三、virt board pin描述及pinctrl maps注册

    四、device与pinctrl的绑定

    五、gpio与pinctrl子系统相关知识点说明

    六、功能验证

    一、Virt soc pin描述

    既然pinctrl device是对soc pin controller的驱动程序,因此我们需要定义下我们虚拟的soc引脚定义。

    如下图所示,本virt soc 提供32个pin,每一个pin支持4个可选状态。提供2个32bit寄存器描述该soc引脚复用信息,因为每个pin支持4个可选状态,因此使用2bits描述该pin的状态。因为只是一个虚拟的soc pin描述,因此此处仅定义了32个pin信息。

    两个寄存器分别定义pinmux_reg0、pinmux_reg1,其中pin0使用pinmux_reg0的bit0、bit1描述其状态:00b表示gpio0;01b iic0_sdat。Pin1使用pinmux_reg0的bit2、bit3描述其状态:00b表示gpio1;10表示uart0_tx;

    该soc可支持32个gpio、3个iic、2个uart、1个spi、2个can、1个nandflash的功能复用,而这些功能中存在着引脚复用。

    67c4c040f2afe509b8ca4ced22c3aac3.png

    二、 virt pinctrl dev驱动实现

    前面的文章中,已经说明了pinctrl dev的驱动开发流程,此处再次说明一下:

    主要包含如下几个步骤:

    1. 为该soc pin controller 实现platform device driver驱动,然后在该驱动的probe接口中实现如下功能:
    2. 定义struct pinctrl_desc类型的变量,并实现相应的成员变量的配置,包含支持的引脚描述、支持的引脚复用接口的赋值、支持的引脚配置接口的赋值、支持的group操作接口以及dt2map接口的赋值等;
    3. 调用pinctrl_register/devm_pinctrl_register完成pinctrl device的注册
    4. 定义该soc pin controller的group相关变量的添加(若使用自行定义的结构存储就自行实现,也可调用pinctrl_generic_add_group接口实现);
    5. 定义该soc pin controller的function相关变量的添加(若使用自行定义的结构存储就自行实现,也可调用pinctrl_generic_add_function接口实现);

    Virt pinctrl dev数据结构

    我们定义了三个数据结构,分别为struct virt_function_desc、struct virt_group_desc、struct virt_pinctrl,其中struct virt_function_desc是对一个function的描述,struct virt_group_desc是对一个group的描述,而struct virt_pinctrl则描述一个soc pin controller。

    struct virt_function_desc

    该数据结构描述一个function,包含function名称、该function所包含的group名称数组、group的个数、引脚复用的配置参数、引脚复用配置参数的掩码(针对我们的soc,mask为0x03(占用2位),而mux_val即为引脚复用配置值,如针对iic function,则其mux_val为0x01)

    bc3e7e88dbc68ee59c7a1aa3e3856b8d.png

    struct virt_group_desc

    该数据结构描述一个group,包含group名称,该group包含的引脚个数、引脚id数组。

    3697ce0171c814cf9232be9e82cce705.png

    struct virt_pinctrl

    该数据结构描述一个soc pin controller,包含:

    1. Struct pinctrl_dev类型的指针变量;
    2. 引脚复用寄存器(此处定义为pin_mux_reg,在实际的应用中,应是寄存器基地址的map,即reg_base变量,此处用pin_mux_reg替代);
    3. 该soc pin controller所包含的group信息;
    4. 该soc pin controller所包含的function信息
    dd42d8b446f385e528da9fe6e0f1b812.png

    struct pinctrl_desc类型变量定义

    如下是该soc pin controller对应的struct pinctrl_desc类型变量的定义,包含描述该soc pin controller的引脚信息的变量(virt_pins)、引脚复用操作接口(virt_pinmux_ops)、group获取相关的操作接口(virt_pinctrl_ops),此处我们没有实现引脚配置的操作接口,感兴趣的童鞋可自行实现。

    3cd1d78d2d9bd233786424afbf422d4b.png
    047c066ef5bb3bd6bba9747fa00f259e.png
    0eb8d3b5cc06ecbdaddf3f2fb3f4e9a8.png

    Pinctrl device的注册

    调用pinctrl_register/devm_pinctrl_register接口即完成virt soc controller 驱动的注册。

    如下即为该virt pinctrl dev驱动对应的platform driver probe函数的实现,相对来说比较简单

    37756fe336f6e19efa579c048a2843dc.png

    在上面我们为该platform device注册了属性参数,主要用于读取引脚复用配置寄存器virt_pinctrl_ptr->pin_mux_reg的信息,定义如下:

    209afccad559cb4bdfbf35e619533ffb.png

    三、virt board pin描述及pinctrl maps注册

    上面说明soc pin controller 驱动的实现,下面我们说明virt board pin 描述及pinctrl maps的注册。由于在ubunt1804上测试,其内核是没有支持设备树的,因此我们通过定义struct pinctrl_map数组,并调用pinctrl_register_mappings实现baord相关的pinctrl maps注册。

    因为仅是测试验证,此处我们仅描述spi0的pinctrl_map(若是正常的驱动,则需要描述本board所需要配置的所有pinctrl_map信息),我们的pinctrl_map,其对应的spi设备名称为virt_spi.0(spi master设备所对应的platform device的名称,因为spi master并没有使用设备与驱动绑定操作,因此此处不能是spi master对应device的名称)、virt_pinctrl_dev是我们上面定义的virt pinctrl dev对应的struct device类型变量的名称、spi0_group表示我们选择的virt soc pin controller的组名称、spi0_func表示我们选择的virt soc pin controller的function名称(对应最上面的引脚状态定义表格的内容)。

    调用pinctrl_regiser_mappings后,则将该pinctrl_map注册到pinctrl_maps链表上。

    26275cec6e1bb450903585ceed8c09f0.png

    若内核支持设备树,则需要在各自外设的的节点中增加针对pinctrl function、pinctrl group的描述即可。如下图时zynq-zc702的i2c0控制器的节点描述,通过pinctrl-names(描述该function的状态,包含default、idle、sleep等,在之前的文章中已经说明,需要了解的可查看之前的文章)、pinctrl-0(对应的的function定义)即可描述

    84e0cdd3791050883a4dc1b38ff2ea14.png
    9595f95ce273a49397376f6bc7719396.png

    四、device与pinctrl的绑定

    在上面我们定义了针对spi0的pinctrl map,那什么时候才会配置spi0的引脚复用呢?我们在前面的《Linux pinctrl子系统分析之六 设备与pinctrl子系统的bind》文章中已经说明,当spi0对应的platform device、platform driver 匹配成功后,probe时进行设备与pinctrl子系统的绑定,并完成引脚的参数配置、复用配置操作。而在此次测试中,我们使用之前在《spi分析专栏》中实现的虚拟spi控制器驱动,完成虚拟spi控制器对应的platform device、platform driver的注册及绑定,从而完成针对spi0引脚的复用配置操作(虚拟spi控制器驱动实现就不再此处细说了)。

    五、gpio与pinctrl子系统相关知识点说明

    针对gpio的使用,一般也是需要进行引脚复用配置,如我们在此处定义的引脚状态表中,这32个引脚既可以作为gpio引脚、也可以作为不同控制器的引脚。而针对gpio控制器而言,和普通的设备引脚复用又有所不同,针对普通的设备而言,若作为设备引脚使用,则这些引脚均被设备使用(如iic0 sda、iic0 scl)。但是针对gpio控制器而言,如我们实现虚拟gpio控制器,其包含32个gpio引脚,但是由于引脚复用的关系,该gpio控制器中可能只有部分引脚可以作为gpio,因此针对gpio的引脚复用配置,pinctrl与gpio子系统做了兼容设置。

    在调用gpio_request时,则会调用pinctrl 子系统提供的pin_request操作,通过pin_request确定该引脚是否已被其他模块使用(gpiochip_generic_request接口或者pinctrl_request_gpio、pinctrl_gpio_requeset)。而针对gpio与pinctrl,存在gpio引脚index与pinctrl pin index的转换工作,因此定义数据结构描述gpio引脚与pinctrl 引脚的转换;主要数据结构为struct gpio_pin_range、pinctrl_gpio_range,主要也就是gpio控制器的gpio base、num_gpio、pinctrl pin引脚的base index等信息。只需要在gpio_chip注册时,将struct gpio_pin_range类型的变量,添加到struct pinctrl_dev的成员变量链表gpio_ranges上即可。

    79e741f9b18a0b3e6d32ac5ac36f309c.png
    2e998e40f7134fade039b7d2d05b2510.png

    本篇文章我们的虚拟gpio控制器驱动(该驱动是在之前《gpio专栏》中实现的,此处不再细述),增加实现了该功能。主要是在虚拟gpio控制器驱动对应platform driver probe中增加针对gpio range的注册代码,实现如下:

    64f9f2ae3c93a8f71286a3cb121ce06a.png

    六、功能验证

    1. 首先将pinctrl device驱动注册到系统中:
    2. insmod ./images/virt_pinctrl_dev.ko;
    3. insmod ./images/pinctrl-virt0612.ko

    执行完成以上工作后,即完成soc pinctrl dev、pinctrl map的注册,而我们的pinctrl device对应的platform device路径为/sys/devices/platform/virt_pinctrl_dev,我们可以在该目录下查看引脚复用寄存器的设置值。如下:

    803a620a9c144f8b120884a663d0931e.png
    1. 将spi controller 注册到系统中
    2. insmod ./images/virtual_spi_controller.ko

    执行完成insmod后,查看寄存器的值

    b2c24787a87c479c4c788965de292785.png

    已经完成引脚复用的配置。

    1. 将gpio controller驱动注册到系统中
    2. insmod ./images/virt_gpio.ko
    3. insmod ./images/virt_gpio_dev.ko

    测试验证下:

    5046ca4d35eba070fd96abb84d00711d.png

    我们注册的gpio的base index为256,我们会发现能够设置gpio0(即256),但是不能设置gpio6(262),那是引脚6我们已经用作spi0 clk了。下面我们注销spi 0 controller:

    注销spi0后,就可以使用gpio6了,那是在spi controller注销时,会调用pin_free释放该引脚,因此就可以将pin6作为gpio使用了。

    8e727a85ab0c3db1b1f9a8b46c318146.png

    以上就是本章的主要内容,我们实现了一个虚拟的pinctrl device驱动,且借助虚拟的spi控制器驱动、虚拟的gpio控制器驱动、sysfs的属性文件,完成了完整的模拟工作。希望对学习pinctrl子系统的童鞋有所帮助。(本篇文章涉及的所有代码,会放到gitee上,稍后会把链接放出来)

    展开全文
  • by fanxiushu 202-03-01 转载或引用请注明原始作者。...从CSDN上的第一篇文章开始:https://blog.csdn.net/fanxiushu/article/details/8496747 (虚拟摄像头驱动原理开发) 文章描述的是利用老的流内核来...
  • win7虚拟打印驱动开发注意事项通过控制面板安装遇到以下问题:错误1、提示“Printer driver was not installed. Operation could not be completed (error 0x00000002)”错误2、没有任何提示,就是安装不上最后还是...
  • Windows 2000下虚拟串口 WDM 驱动程序的开发 孙筱萌,夏 斌,韩德红,方 晓 (空军雷达学院电子对抗系,武汉 430019 摘 要:针对传统 RS-232串行通信存在的通信距离 端口数量等多个方面的限制, 提出了在 Windows 2000 操作...
  • 虚拟驱动即与硬件无关,用软件来实现硬件功能。 在window中驱动一般有对应的硬件,即一个FDO对应一个PDO。 问题是这样,虚拟驱动FDO有对应的PDO么,如果有谁来创建,怎么设置? 或者假如一个正常的键盘驱动,怎样...
  • VC Mirror Driver显示虚拟驱动经典开发

    千次阅读 2016-11-06 10:44:05
    一个简单的显示驱动实例 windows wdk 7600的 mirror(镜像) 显示驱动部分 Mirror Driver程序的基本流程 Windows 2000 DDK包含了一个例子镜像驱动程序,在下面3个目录中包括了组件源文件。 目录 包含的...
  • 在前面几章,我们分析了input子系统的框架,以及input handler的注册与实现,本章我们通过一个虚拟的input device驱动,说明如何开发一个input device驱动。...一、input device驱动开发流程针对input device的...
  • 3 驱动开发工具:WDK10 4Windows SDK:SDK10(安装的时候必须全部勾选安装,否则编译会出现缺少waring.h等头文件之类的错误) 5 VM虚拟机:VMware WorkStation 12 Win10、WDK10、SDK10的版本必须一致我的用的都是...
  • by fanxiushu 2019-06-24 转载或引用请注明原始作者。...这里与远程桌面关系不是太大,但这个部分是xdisp_virt远程控制程序的实现多显示器桌面扩展的子功能,因此也归为远程桌面开发一类。 这篇文章与之前发布的...
  • fanxiushu 2016-10-08 转载或引用,请注明原始作者 做这个事情写这篇文章之前,压根没朝模拟USB摄像头这方面去想过。 直到CSDN上一位朋友...记得最早的一篇文章也是介绍虚拟摄像头驱动开发的,只是当时采用的是wi
  • 应用程序与驱动的交互原理 驱动程序:用来与硬件打交道,获取硬件的数据; 应用程序:与驱动程序打交道,借助驱动程序完成对硬件的操作; 在单片机中,驱动程序和应用程序的区分并不明显,因为没有操作系统的存在,...
  • 摘要:介绍虚拟设备驱动程序开发的基本知识以及VxDs与WIN32应用程序通讯的几种常用方法,并给出了用VtoolsD开发VxDs的具体实例。 Windows自面世以来,即以其强大而友好的图形界面占据了操作系统(尤其是微机操作...
  • 我现在在做一个C#的桌面软件,需要使用虚拟摄像头,查了些关于虚拟摄像头开发的资料,稍微了解了一点,但是还是不是很清楚具体该怎么着手做。 请问下如何对虚拟摄像头采用什么编程语言,什么开发工具比较合适,...
  • 美国微软公司出品的Windows98以其友好的图形用户界面,在我国赢得了广泛的市场。在给广大办公环境工作人员...Windows98内核管理机制非常复杂,因而编写虚拟驱动程序也变得十分困难,要想编写虚拟驱动程序,就必须对Wind
  • 虚拟地址空间 当处理器读或写入内存位置时,它会使用虚拟地址。作为读或写操作的一部分,处理器将虚拟地址转换为物理地址。通过虚拟地址访问内存有以下优势: 程序可以使用一系列相邻的虚拟地址来...
  • 要求:应用程序可以对驱动进行读写操作。 读:从驱动读一个字符串 写:应用程序向驱动写一个字符串 驱动的缓冲,与应用程序的读写buffer都是100. 1. read驱动函数编写,需要用到copy_to_user(buf, readbuf, cnt)...
  • 一个虚拟的input device驱动,说明如何开发一个input device驱动。本章涉及的内容如下: Platform device、driver的使用 ...针对input device的驱动开发主要涉及如下几个开发步骤: 调用input_allocate_...
  • >>中,讲到在wince 下开发虚拟串口驱动的方法,现在介绍在windows XP下开发虚拟串口的方法。   可以开发一个虚拟串口,将读写请求传递给USB驱动,这样就可以利用现成的串口调试工具向USB设备读取了。 1、DDK串
  • WinXP下虚拟摄像头驱动程序开发 摄像头驱动程序的主要目的是通过硬件捕捉视频信号。微软公司提供了一套视频驱动的接口,可以满足这个接口的视频驱动程序。第三方厂商开发的软件,如QQ和MSN等软件,都可以通过这个...
  • 虚拟摄像头驱动原理及开发

    万次阅读 热门讨论 2013-01-12 19:23:57
     类似功能的产品,如著名的e2eSoft的 VCam,国内新浪的9518虚拟视频, 新浪的虚拟视频是DirectShow应用层上的视频模拟,大概的思路就是: 因为所有的摄像头WDM驱动,都需要通过运行在应用层的ksproxy.ax与...
  • Windows驱动开发——虚拟串口设备

    千次阅读 2013-10-14 19:45:24
    只要编写的驱动满足这些接口,并按照串口标准的命名方法,不管是真实的串口设备,还是虚拟设备,Windows操作系统都会认为 这个设备是一个标准的串口设备。用标准的串口调试工具都可以与这个设备
  • 本课程是linux驱动开发的第11个课程,主要内容是linux的网络驱动的介绍,首先讲述了网络设备驱动接口和之前讲的2种的不同,然后以一个虚拟网卡驱动源码学习了网卡驱动的框架,后分析了一个实际网卡DM9000的驱动细节...
  • by fanxiushu 2020-09-18...关于虚拟USB总线驱动的讲解的内容比较多,时间跨度也比较长。 有基于linux平台下如何实现虚拟USB总线驱动的,CSDN上的如下链接:https://blog.csdn.net/fanxiushu/article/details/1029674...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,912
精华内容 764
关键字:

虚拟驱动开发