电子说
周立功教授新书《面向AMetal框架与接口的编程(上)》,对AMetal框架进行了详细介绍,通过阅读这本书,你可以学到高度复用的软件设计原则和面向接口编程的开发思想,聚焦自己的“核心域”,改变自己的编程思维,实现企业和个人的共同进步。
第八章为深入理解AMetal,本文内容为8.1 LED 通用接口。
第八章导读
面向通用接口的编程使得应用程序与具体硬件无关,可以很容易地实现跨平台复用。但究其本质如何,具体是怎样实现的呢?
8.1 LED 通用接口
本节将以LED 通用接口为例,详细介绍通用接口的设计方法。
>>> 8.1.1 定义接口
合理的接口应该是易阅读的、职责明确的,下面将从接口的命名、参数和返回值三个方面阐述在AMetal 中定义接口的一般方法。
1. 接口命名
在AMetal 中,所有通用接口均以“am_”开头,紧接着是操作对象的名字,对于LED控制接口来说,所有接口应该以“am_led_”为前缀。
当接口的前缀定义好之后,需要考虑定义哪些功能性接口,然后根据功能完善接口名。对于LED 来说,核心的操作是控制LED 的状态,点亮或熄灭LED,因此需要提供一个设置(set)LED 状态的函数,比如:
am_led_set
显然,通过该接口可以设置LED 的状态,为了区分是点亮还是熄灭LED,需要通过一个参数指定具体的操作。
在大多数应用场合中,可能需要频繁地开灯和关灯操作,每次开关灯都需要通过传递参数给am_led_set()接口实现开灯和关灯,这样做会非常繁琐。因此可以为常用的开灯和关灯操作定义专用的接口,也就不再需要额外参数区分具体的操作。比如,使用on 和off 分别表示开灯和关灯,则定义开灯和关灯的接口名为:
am_led_on
am_led_off
在一些特殊的应用场合种,比如,LED 闪烁,其可能并不关心具体的操作是开灯还是关灯,它仅仅需要LED 的状态发生翻转。此时,可以定义一个用于翻转(toggle)LED 状态的接口,其接口名为:
am_led_toggle
2. 接口参数
在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 号表示操作的具体对象外,还可能直接使用指向具体对象的指针,或者表示具体对象的一个句柄来表示,它们的作用在本质上是完全一样的。
3. 返回值
对于用户来说,调用通用接口后,应该可以获取本次执行的结果,成功还是失败,或一些其它的有用信息。比如,当调用接口时,如果指定的led_id 超过有效范围时,由于没有与led_id 对应的LED 设备,操作必定会失败,此时必须返回错误,告知用户操作失败,且失败的原因是led_id 不在有效范围内,无与之对应的LED 设备。
在AMetal 中,通过返回值返回接口执行的结果,其类型为int,返回值的含义为:若返回值为AM_OK,则表示操作成功;若返回值为负数,则表示操作失败,失败原因可根据返回值,查找am_errno.h 文件中定义的宏,根据宏的含义确定失败的原因;若返回值为正数,其含义与具体接口相关,由具体接口定义,无特殊说明时,表明不会返回正数。
AM_OK 在am_common.h 文件中定义,其定义如下:
错误号在am_errno.h 文件中定义,几个常见错误号的定义详见表8.1。比如,在调用LED 通用接口时,若led_id 不在有效范围内,则该led_id没有对应的LED 设备,此时接口应该返回-AM_ENODEV。注意:M_ENODEV 的前面有一个负号,以返回负值。
表8.1 常见错误号定义(am_errno.h)
基于此,将所有LED 控制接口的返回值定义为int,LED 控制接口的完整定义详见表8.2,其对应的类图详见图8.1。
表8.2 LED 通用接口(am_led.h)
图8.1 LED 对应的类图
>>> 8.1.2 实现接口
当完成接口定义后,还需要提供相应的驱动实现这些接口,才能使用这些接口操作LED。
1. 实现接口初探
LED 有4 个通用接口函数,其中的am_led_on()和am_led_off()接口是基于am_led_set()接口实现的,详见程序清单8.1。
程序清单8.1 am_led_on()和am_led_off()接口的实现
实现接口的核心是实现am_led_set()和am_led_toggle()接口,通用接口在于屏蔽底层的差异性,即无论底层硬件如何变化,用户都可以调用通用接口操作LED。但对于不同的硬件电路,比如,GPIO 和HC595 控制LED 的硬件电路,设置LED 状态和LED 翻转的具体实现是不同的。下面以设置LED 状态的具体实现为例进行详细说明。
对于GPIO 控制LED 的硬件电路来说,当使用GPIO 控制AM824-Core 的两个板载LED时,LED0 通过J9 与MCU 的PIO0_20 相连,LED1 通过J10 与MCU 的PIO0_21 相连。使用短路冒将J9 和J10 短路后即可使用PIO0_20 和PIO0_21 控制LED0 和LED1,当引脚输出低电平时,则点亮LED;当引脚输出高电平时,则熄灭LED,直接使用GPIO 通用接口实现am_led_set()接口详见程序清单8.2。
程序清单8.2 am_led_set()的实现(GPIO 控制LED)
对于HC595 控制LED 的硬件电路来说,当将MiniPort-595 和MiniPort-LED 联合使用时,通过控制HC595 的输出,可以达到控制LED 点亮和熄灭的效果,当相应引脚输出低电平时,则点亮LED;当相应输出高电平时,则熄灭LED,直接使用HC595 通用接口实现am_led_set()接口详见程序清单8.3。
程序清单8.3 am_led_set()的实现(HC595 控制LED)
在实际的应用中,__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_cookie,p_cookie 代表了具体的对象,实际上驱动函数和p_cookie一起唯一地确定了一个具体的LED 设备。基于此可以将驱动函数和p_cookie 定义在一起,形成一个新的LED 设备类型。即:
其中,p_funcs 为指向驱动虚函数表的指针,比如,指向__g_led_gpio_drv_funcs 或__g_led_hc595_drv_funcs,p_cookie 为指向设备的指针,即传递给驱动函数的第一个参数。
此时,在am_led_set()接口的实现中,无需完成真实的设置LED 状态的操作,仅需调用设备中的pfn_led_set 方法即可,其范例程序详见程序清单8.6。
程序清单8.6 am_led_set()实现(1)
假定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。
程序清单8.7 am_led_set()实现(2)
其中,__led_dev_find_with_id()的作用就是遍历设备链表,与各个设备中的ID 信息一一比对,以找到led_id 对应的LED 设备,其实现详见程序清单8.8。
程序清单8.8 查找指定led_id 的LED 设备
其中,__gp_head 是一个全局变量,初始为NULL,表示初始时系统中无任何LED 设备。同理,可得到am_led_toggle()接口的实现,详见程序清单8.9。
程序清单8.9 am_led_toggle()实现
至此,实现了所有通用接口。由于当前没有任何LED 设备,因此__led_dev_find_with_id()为NULL,通用接口的返回值也始终为-AM_ENODEV。
为了使通用接口能够操作到具体有效的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。
图8.2 抽象的LED 设备类
LED 设备中存在抽象方法pfn_led_set 和pfn_led_toggle,这两个抽象方法是以虚函数表的形式存在LED 设备类中的。由于存在抽象方法,因此LED 设备类是一个抽象类,它本身不能够直接实例化,必须由其派生的具体类实现这两个抽象方法。为了便于查阅,如程序清单8.11 所示展示了LED 设备的接口文件am_led_dev.h 的内容。
程序清单8.11 am_led_dev.h 文件内容
3. 具体的LED 设备类
前面定义的抽象LED 设备类中包含了两个抽象方法:pfn_led_set 和pfn_led_toggle。为了使用户可以通过LED 通用接口操作LED,就必须根据实际硬件连接,实现两个抽象方法,然后将具体设备添加到系统设备链表中。
下面分别以GPIO 控制LED 的驱动实现和HC595 控制LED 的驱动实现为例,阐述LED设备驱动开发的一般方法,如果后续有其它类型的LED 控制电路,可以按照此方法添加自定义的LED 驱动。
(1) GPIO 控制LED 的驱动实现
具体LED 设备的核心功能是实现抽象设备类中定义的方法,首先应该基于抽象设备类派生一个具体的设备类,其类图详见图8.3。
图8.3 具体设备类(GPIO)
可直接定义具体的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。这些工作通常在驱动的初始化函数中完成,初始化函数的原型为:
其中,p_dev 指向am_led_gpio_dev_t 类型的设备,p_info 为指向am_led_gpio_info_t 类型实例信息的指针,其调用形式如下:
初始化函数的的实现详见程序清单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 的值都将影响本次GPIO 的输出电平,GPIO 的输出电平与state 和active_low 的真值表详见表8.3。由此可见,当state 与active_low 相同时,则GPIO 输出0;当state 与active_low 不同时,则GPIO 输出1,恰好是异或关系。
表8.3 GPIO 输出增值表
为了便于查阅,如程序清单8.14 所示展示了LED 设备接口文件am_led_gpio.h 的内容。
程序清单8.14 am_led_gpio.h 文件内容
(2) HC595 控制LED 的驱动实现
同样,首先基于抽象设备类派生一个具体的设备类,其类图详见图8.4,可直接定义具体的LED 设备类:
am_led_hc595_dev_t 为具体的LED 设备类,当具有该类型后,即可使用该类型定义一个具体的LED 设备实例:
图8.4 具体设备类(HC595)
在使用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 设备添加到系统中。这些工作通常在驱动的初始化函数中完成,初始化函数的原型为:
其中, p_dev 为指向am_led_hc595_dev_t 类型实例的指针, p_info 为指向am_led_hc595_info_t 类型实例信息的指针,其调用形式如下:
初始化函数的的实现详见程序清单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 的内容。
程序清单8.17 am_led_hc595.h 文件内容
全部0条评论
快来发表一下你的评论吧 !