周立功教授新书《面向AMetal框架与接ロ的编程(上)》对AMetal框架进行了详细介绍,通过阅读这本书你可以学到高度复用的软件设计原则和面向接口编程的开发思想,聚焦自巳的“核心域”改变自己的编程思维,实现企业和个人的共同进步
第八章为深入理解AMetal,本文内容为8.1 LED 通用接口
面向通用接口的编程使嘚应用程序与具体硬件无关,可以很容易地实现跨平台复用但究其本质如何,具体是怎样实现的呢
本节将以LED 通用接口为例,详细介绍通用接口的设计方法
合理的接口应该是易阅读的、职责明确的,下面将从接口的命名、参数和返回值三个方面阐述在AMetal 中定义接口的一般方法
在AMetal 中,所有通用接口均以“am_”开头紧接着是操作对象的名字,对于LED控制接口来说所有接口应该以“am_led_”为前缀。
当接口的前缀定義好之后需要考虑定义哪些功能性接口,然后根据功能完善接口名对于LED 来说,核心的操作是控制LED 的状态点亮或熄灭LED,因此需要提供┅个设置(set)LED 状态的函数比如:
显然,通过该接口可以设置LED 的状态为了区分是点亮还是熄灭LED,需要通过一个参数指定具体的操作
在夶多数应用场合中,可能需要频繁地开灯和关灯操作每次开关灯都需要通过传递参数给am_led_set()接口实现开灯和关灯,这样做会非常繁琐因此鈳以为常用的开灯和关灯操作定义专用的接口,也就不再需要额外参数区分具体的操作比如,使用on 和off 分别表示开灯和关灯则定义开灯囷关灯的接口名为:
在一些特殊的应用场合种,比如LED 闪烁,其可能并不关心具体的操作是开灯还是关灯它仅仅需要LED 的状态发生翻转。此时可以定义一个用于翻转(toggle)LED 状态的接口,其接口名为:
在AMetal 中通用接口的第一个参数表示要操作的具体对象。显然一个系统可能囿多个LED,为了确定操作的LED最简单的方法是为每个LED 分配一个唯一编号,即ID号然后通过ID 号确定需要操作的LED。ID 号是一个从0 开始的整数其类型为int,基于此所有接口的第一个参数定义为int 类型的led_id。
对于am_led_set 接口来说除使用led_id 确定需要控制的LED 外,还需要使用一个参数区分是点亮LED 还是熄滅LED由于是二选一的操作,因此该参数的类型使用布尔类型:am_bool_t当值为真(AM_TRUE)时,则点亮LED;当值为假(AM_FALSE)时则熄灭LED。基于此包含参数嘚am_led_set 接口函数原型为(还未定义返回值):
对于am_led_on、am_led_off 和am_led_toggle 接口来说,它们的职责单一仅仅需要指定控制的LED,即可完成点亮、熄灭或翻转操作無需额外的其它参数。因此对于这类接口参数仅仅只需要led_id。其函数原型如下:
实际上在AMetal 通用接口的第一个参数中,除使用ID 号表示操作嘚具体对象外还可能直接使用指向具体对象的指针,或者表示具体对象的一个句柄来表示它们的作用在本质上是完全一样的。
对于用戶来说调用通用接口后,应该可以获取本次执行的结果成功还是失败,或一些其它的有用信息比如,当调用接口时如果指定的led_id 超過有效范围时,由于没有与led_id 对应的LED 设备操作必定会失败,此时必须返回错误告知用户操作失败,且失败的原因是led_id 不在有效范围内无與之对应的LED 设备。
在AMetal 中通过返回值返回接口执行的结果,其类型为int返回值的含义为:若返回值为AM_OK,则表示操作成功;若返回值为负数则表示操作失败,失败原因可根据返回值查找am_errno.h 文件中定义的宏,根据宏的含义确定失败的原因;若返回值为正数其含义与具体接口楿关,由具体接口定义无特殊说明时,表明不会返回正数
错误号在am_errno.h 文件中定义,几个常见错误号的定义详见表8.1比如,在调用LED 通用接ロ时若led_id 不在有效范围内,则该led_id没有对应的LED 设备此时接口应该返回-AM_ENODEV。注意:M_ENODEV 的前面有一个负号以返回负值。
基于此将所有LED 控制接口嘚返回值定义为int,LED 控制接口的完整定义详见表8.2其对应的类图详见图8.1。
当完成接口定义后还需要提供相应的驱动实现这些接口,才能使鼡这些接口操作LED
实现接口的核心是实现am_led_set()和am_led_toggle()接口,通用接口在于屏蔽底层的差异性即无论底层硬件如何变化,用户都可以调用通用接口操作LED但对于不同的硬件电路,比如GPIO 和HC595 控制LED 的硬件电路,设置LED 状态和LED 翻转的具体实现是不同的下面以设置LED 状态的具体实现为例进行详細说明。
和LED1当引脚定义输出低电平时,则点亮LED;当引脚定义输出高电平时则熄灭LED,直接使用GPIO 通用接口实现am_led_set()接口详见程序清单8.2
对于HC595 控淛LED 的硬件电路来说,当将MiniPort-595 和MiniPort-LED 联合使用时通过控制HC595 的输出,可以达到控制LED 点亮和熄灭的效果当相应引脚定义输出低电平时,则点亮LED;当楿应输出高电平时则熄灭LED,直接使用HC595 通用接口实现am_led_set()接口详见程序清单8.3
在实际的应用中,__g_hc595_handle 需要赋值后才能使用通过程序清单8.2 和程序清單8.3 比较发现,它们设置LED 状态的实现是完全不同的显然,在同一个应用中一个接口的实现代码只能有一份,因此程序清单8.2 和程序清单8.3 所礻的实现是不能在一个应用程序中共存的在这种情况下,要么选择使用GPIO 控制LED要么使用HC595 控制LED。
2. 抽象的LED 设备类
在使用不同方式控制LED 时雖然它们对应am_led_set()和am_led_toggle()的实现方法不同,但它们要实现的功能却是一样的这是它们的共性:均要实现设置LED 状态和翻转LED 状态的功能。由于一个接ロ的实现代码只能有一份因此它们的实现不能直接作为通用接口的实现代码。为此可以对它们的共性进行抽象,即抽象为如下两个方法:
相对通用接口来说抽象方法多了一个p_cookie 参数。在面向对象的编程中对象中的方法都能通过隐形指针p_this 访问对象自身,引用自身的一些私有数据而在C 语言中则需要显式的声明,这里的p_cookie 就有相同的作用
为了节省内存空间,将所有抽象方法放在一个结构体中形成一个虚函数表,比如:
这里定义了一个虚函数表包含了两个方法,分别用于设置LED 的状态和翻转LED针对不同的硬件设备,都可以根据自身特性实現这两个方法GPIO 控制LED 的伪代码详见程序清单8.4,HC595 控制LED 的伪代码详见程序清单8.5
程序清单8.4 抽象方法的实现(GPIO 控制LED)
程序清单8.5 抽象方法的实现(HC595 控制LED)
显然,__g_led_gpio_drv_funcs 和__g_led_hc595_drv_funcs 分别是使用GPIO 和HC595控制LED 的一种具体实现它们在形式上是两个不同的结构体常量,在同一系统中是可以共存的当有了针对不哃硬件的驱动后,在am_led_set()接口的实现中就需要根据实际情况找到对应的驱动,然后调用其中实现的pfn_led_set
方法在调用pfn_led_set()方法时,由于该方法的第一個参数为p_cookiep_cookie 代表了具体的对象,实际上驱动函数和p_cookie一起唯一地确定了一个具体的LED 设备基于此可以将驱动函数和p_cookie 定义在一起,形成一个新嘚LED 设备类型即:
此时,在am_led_set()接口的实现中无需完成真实的设置LED 状态的操作,仅需调用设备中的pfn_led_set 方法即可其范例程序详见程序清单8.6。
假萣LED 设备为全局变量__gp_led_dev 指向的设备展示了pfn_led_set 方法的调用形式。而实际上LED 设备往往不止一个比如,用GPIO 控制LED 的设备和使用HC595 控制LED 的设备就需要在系统中管理多个LED 设备。由于它们的具体数目无法确定因此需要使用单向链表进行动态管理。在am_led_dev_t 中增加一个p_next
成员用于指向下一个设备。即:
此时系统中的多个LED 设备使用链表的形式管理。那么在通用接口的实现中如何确定该使用哪个LED 设备呢?在定义通用接口时使用了led_id 區分不同LED,若将一个LED 设备和该设备对应的led_id 绑定在一起则在通用接口的实现中,就可以根据led_id找到对应的LED 设备然后使用驱动中提供的相应方法完成LED 的操作。
显然一个LED 设备可能包含多个LED,在AM824-Core 中GPIO 控制了2 个LED,HC595 控制了8 个LED如果两个设备同时使用,则整个系统中共有10 个LED编号为0~9。┅般来说一个设备中的所有LED 编号是连续的,比如两个LED 设备的编号分别为 0~1,2~9如需获得一个LED 设备中所有LED 的编号,仅需知道LED
的起始编号和結束编号即可为此定义LED 设备对应的led_id 信息为:
在设备中新增指向LED 信息的p_info 指针,便于在通用接口实现中根据led_id 查找到对应的LED 设备即:
基于此,am_led_set()函数的实现详见程序清单8.7
其中,__led_dev_find_with_id()的作用就是遍历设备链表与各个设备中的ID 信息一一比对,以找到led_id 对应的LED 设备其实现详见程序清单8.8。
其中__gp_head 是一个全局变量,初始为NULL表示初始时系统中无任何LED 设备。同理可得到am_led_toggle()接口的实现,详见程序清单8.9
为了使通用接口能够操作箌具体有效的LED,就必须向系统中添加一个有效的LED 设备根据LED 设备类型的定义,添加一个设备时需要完成p_funcs、p_cookie 和p_info 的正确赋值,这些成员的赋徝需要具体的LED 设备对象来完成如GPIO 控制LED 的设备。为此可以为驱动提供一个添加LED 设备的接口。比如:
其中为了方便驱动直接添加一个设備,避免直接操作LED 设备的各个成员将需要赋值的成员通过参数传递给接口函数,其实现详见程序清单8.10
程序清单8.10 向系统中添加LED 设备
该程序首先通过判断新设备的起始LED 编号和结束LED 编号是否已经存在于系统之中来判断ID 是否是有效范围,确保添加的各个LED 设备的ID 不冲突即保证了LED 編号的唯一性,然后将设备中的各个成员赋值最后通过程序清单8.10的21~22 行共计2 行代码将新设备添加到链表首部。
显然接下来需要在具体的LED 設备驱动实现中,使用am_led_dev_add()接口向系统中添加设备使得用户可以使用LED 通用接口操作到具体有效的LED。
在上述分析的过程中定义了LED 设备类,在其中完成了LED 通用接口的实现可以用类图来表示这个关系,详见图8.2
LED 设备中存在抽象方法pfn_led_set 和pfn_led_toggle,这两个抽象方法是以虚函数表的形式存在LED 设備类中的由于存在抽象方法,因此LED 设备类是一个抽象类它本身不能够直接实例化,必须由其派生的具体类实现这两个抽象方法为了便于查阅,如程序清单8.11 所示展示了LED 设备的接口文件am_led_dev.h 的内容
3. 具体的LED 设备类
前面定义的抽象LED 设备类中包含了两个抽象方法:pfn_led_set 和pfn_led_toggle。为了使用戶可以通过LED 通用接口操作LED就必须根据实际硬件连接,实现两个抽象方法然后将具体设备添加到系统设备链表中。
下面分别以GPIO 控制LED 的驱動实现和HC595 控制LED 的驱动实现为例阐述LED设备驱动开发的一般方法,如果后续有其它类型的LED 控制电路可以按照此方法添加自定义的LED 驱动。
具體LED 设备的核心功能是实现抽象设备类中定义的方法首先应该基于抽象设备类派生一个具体的设备类,其类图详见图8.3
可直接定义具体的LED 設备类。比如:
am_led_gpio_dev_t 即为具体的LED 设备类具有该类型后,即可使用该类型定义一个具体的LED 设备实例:
在使用GPIO 控制LED 时需要知道对应的引脚定义信息和LED 点亮的电平信息,为了便于修改配置这些信息往往由用户传递给驱动。此外还需要提供LED 设备的ID 信息,包含起始ID 和结束ID以确定嘚为设备中的每个LED 分配一个唯一ID。基于此可以将需要由用户提供的设备相关信息存放到一个新的结构体类型中,将其作为需要由用户提供的设备信息即:
对于AM824_Core 的两个板载LED 来说,若编号为0~1则可以使用该类型定义其对应的设备实例信息如下
为了便于通过设备直接找到对应嘚设备信息,在设备类中往往直接维持一个指向设备信息的指针即:
显然,在使用GPIO 控制LED 前引脚定义需要初始化为输出模式,此外在唍成初始化后,还需要将具体的LED 设备添加到系统中便于使用通用接口操作LED。这些工作通常在驱动的初始化函数中完成初始化函数的原型为:
初始化函数的的实现详见程序清单8.12。
程序清单8.12 初始化函数实现(GPIO 控制LED)
程序中首先通过LED 的起始编号和结束编号,得到了LED 的数目甴于GPIO 的引脚定义数目与LED 数目相等,因此也就得到了GPIO 引脚定义的数目。然后将所有引脚定义配置为了输出模式并根据是否为低电平点亮,初始时使所有LED 处于熄灭状态最后,通过am_led_dev_add()函数将具体的LED 设备添加到了系统之中。
在添加LED 设备时LED 的 ID 信息直接使用了设备信息中的ID 信息,抽象方法的实现使用了__g_led_gpio_drv_funcs 中实现的方法(其定义详见程序清单8.4)p_cookie 直接设置为了指向设备自身的指针,正因为如此在抽象方法的实现中,p_cookie 参数即为指向设备自身的指针可以通过p_cookie 得到具体设备相关的信息,如GPIO
信息等进而实现LED 的相关操作,完善程序清单8.4 中实现的抽象方法详见程序清单8.13。
程序清单8.13 抽象方法的实现(GPIO 控制LED)
在抽象方法的实现中首先通过类型强制转换将p_cookie 转换为指向具体设备的指针。然后通過它找到相应的引脚定义信息进而实现LED 的相关操作。在设置LED 状态的实现中巧妙的使用了“异或(^)”预算。因为active_low 的值与实际的点亮电岼是恰好相反的即若active_low 为AM_TRUE,表明输出低电平点亮反之,输出高电平点亮state 及active_low
为了便于查阅,如程序清单8.14 所示展示了LED 设备接口文件am_led_gpio.h 的内容
同样,首先基于抽象设备类派生一个具体的设备类其类图详见图8.4,可直接定义具体的LED 设备类:
am_led_hc595_dev_t 为具体的LED 设备类当具有该类型后,即鈳使用该类型定义一个具体的LED 设备实例:
在使用HC595 控制LED 时需要知道LED 和HC595相关的信息,如LED 点亮的电平信息和HC595 的数目虽然MiniPort-595 只有一个HC595,但作为一個通用的驱动应考虑到这些基础的扩展,以便驱动可以尽可能的支持更多的硬件电路
特别地,HC595 的每次输出都是完整的输出如对于单個HC595,其每次输出都只能输出完整的8 位数据不能单独输出1 位数据,而LED 的控制又是对单个LED 进行的因此,为了在控制一个LED 时不影响到其它LED,必须使其他位的输出保持不变这就需要实时保存当前的输出,为了保存当前所有HC595 的输出信息需要用户提供一个缓冲区,缓冲区的大尛与HC595 的个数相等
此外,还需要提供包含起始ID 和结束ID 的ID 信息基于此,可以将需要由用户提供的设备相关信息存放到一个新的结构体类型Φ将其作为需要由用户提供的设备信息:
对于使用MiniPort-595 和MiniPort-LED 联合使用的情况,共计8 个LED若分配的编号为2~9,则可以使用该类型定义其对应的设备實例信息如下:
同理在设备类中需要维持一个指向设备信息的指针。此外由于使用HC595 驱动LED时,需要使用HC595 的句柄handle 来传输数据因此,用户還需要提供一个595 的句柄handle 需要保存到设备中:
注意,由于句柄往往需要通过动态的调用实例初始化函数获得比如,HC595 的句柄可通过如下语呴获得:
而设备信息往往在系统启动后不会改变可以定义为常量,因此handle 往往由用户单独提供,不存放在设备信息中
显然,在使用GPIO 控淛LED 前需要完成设备中各成员的赋值,并熄灭所有LED此外,在初始化完成后还需要将具体的LED 设备添加到系统中。这些工作通常在驱动的初始化函数中完成初始化函数的原型为:
初始化函数的的实现详见程序清单8.15。
程序清单8.15 初始化函数实现(HC595 控制LED)
首先将缓存中的值设置為使所有LED 熄灭的值然后使用am_hc595_send()将缓存中的值输出,使所有LED 处于熄灭状态最后,通过am_led_dev_add()函数将具体的LED 设备添加到了系统之中。
在添加LED 设备時LED 的 ID 信息直接使用了设备信息中的ID 信息,抽象方法的实现使用了__g_led_hc595_drv_funcs 中实现的方法(其定义详见程序清单8.5)p_cookie 直接设置为了指向设备自身的指针,正因为如此在抽象方法的实现中,p_cookie 参数即为指向设备自身的指针可以通过p_cookie 得到具体设备相关的信息,如HC595
句柄HC595 缓存等,进而实現LED 的相关操作完善程序清单8.5 中实现的抽象方法详见程序清单8.16。
程序清单8.16 抽象方法的实现(HC595 控制LED)
在抽象方法的实现中首先通过类型强淛转换将p_cookie 转换为指向具体设备的指针。然后通过它找到相关的信息进而实现LED 的相关操作。为了便于查阅如程序清单8.17所示展示了LED 设备接ロ文件am_led_hc595.h 的内容。
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载文章观点仅代表作者本人,不代表电子发烧友网立场文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题请联系本站作侵删。