特洛伊希文

又一个WordPress站点

雅安三雅周立功:深入理解AMetal——蜂鸣器接口和温度采集接口-周立功单片机

2018-05-25 11:01:59
周立功:深入理解AMetal——蜂鸣器接口和温度采集接口-周立功单片机
周立功教授新书《面向AMetal框架与接口的编程(上)》,对AMetal框架进行了详细介绍,通过阅读这本书,你可以学到高度复用的软件设计原则和面向接口编程的开发思想,聚焦自己的“核心域”,改变自己的编程思维,实现企业和个人的共同进步。
第八章为深入理解AMetal,本文内容为8.3 蜂鸣器接口和8.4 温度采集接口。
8.3 蜂鸣器接口
>>> 8.3.1 定义接口
1. 接口命名
由于操作的对象是蜂鸣器(buzzer),因此,接口命名以“am_buzzer_”作为前缀。对于蜂鸣器,基本的操作是打开和关闭蜂鸣器,可定义相应的两个接口名为:
am_buzzer_on
am_buzzer_off
特别地,在一些应用场合,还需要类似蜂鸣器“嘀一声”这样的操作,即鸣叫一定的时间后自动停止。可以定义其接口名为:
am_buzzer_beep
am_buzzer_beep_async
这里定义了两个接口,都是用于蜂鸣器鸣叫指定的时间,二者的区别在于函数返回的时机不同。am_buzzer_beep 会等待鸣叫结束后返回,am_buzzer_beep_async 不会等待,函数立即返回,蜂鸣器鸣叫指定时间后自动停止。
显然,对于am_buzzer_beep_async 接口,在最开始的蜂鸣器接口设计中,很可能是不会想到的,该接口是在大量实际应用中得出的,由于在一些特殊的应用场景,不希望程序被阻塞,因此,需要提供am_buzzer_beep_async 这样的异步接口。
2. 接口参数
在LED 通用接口的设计中,由于在一个系统中,可能存在多个LED,这就必须使用某种方法区分不同的LED,如使用了唯一ID 号led_id 表示来区分系统中的多个LED。按照这种逻辑,是否也需要一个buzzer_id 来区分不同的蜂鸣器呢?
蜂鸣器的功能单一,是一种发声器件,在一个具体应用中,发声器件往往只有一个,没有必要使用多个蜂鸣器。因此,蜂鸣器可以看做系统的一个单实例设备,基于此,也就无需使用类似于buzzer_id 这样的参数来区分多个蜂鸣器了,对于打开和关闭蜂鸣器的接口,则无需任何参数,即:
am_buzzer_on(void);
am_buzzer_off(void);
特别地,对于am_buzzer_beep 和am_buzzer_beep_async 接口,虽无需参数来区分多个蜂鸣器,但由于其功能是鸣叫一定的时间,因此,还需要一个用于指定鸣叫时长的参数。
am_buzzer_beep(uint32_t ms);
am_buzzer_beep_async (uint32_t ms);
其中,ms 用于指定鸣叫时长,单位为毫秒。
3. 返回值
接口无特殊说明,直接将所有接口的返回值定义为int 类型的标准错误号。基于此早安社区,蜂鸣器控制接口的完整定义详见表8.5。
表8.5 蜂鸣器通用接口(am_buzzer.h)

其对应的类图详见图8.8。

图8.8 蜂鸣器接口类图
>>> 8.3.2 实现接口
1. 抽象的蜂鸣器设备类
蜂鸣器共计4 个通用接口,其中,am_buzzer_beep()和am_buzzer_beep_async()接口可以直接基于am_buzzer_on()和am_buzzer_off()接口实现,am_buzzer_beep()的实现详见程序清单8.24。
程序清单8.24 am_buzzer_beep()的实现

程序中青莲空间,首先使用am_buzzer_on()打开蜂鸣器奔月蜀客,若打开蜂鸣器失败(返回值为负数),则直接返回相应的错误号,若打开成功,则使用am_mdelay()延时指定的时间,最后关闭蜂鸣器。对于am_buzzer_beep_async()接口,其需要立即返回,不能在函数内部直接使用延时函数,可以基于软件定时器实现,范例程序详见程序清单8.25。
程序清单8.25 am_buzzer_beep_async()的范例程序

程序中,首先使用am_buzzer_on()打开蜂鸣器,若打开蜂鸣器失败(返回值为负数),则直接返回相应的错误号,若打开成功李尹馨,则启动软件定时器,定时时间为指定的鸣叫时间,启动定时器后,函数立即返回。软件定时器定时时间到后,需要调用自定义回调函数__beep_timer_callback(),在回调函数中,关闭了软件定时器和蜂鸣器,鸣叫结束。
显然,软件定时器在使用前,需要初始化,以将__beep_timer_callback()函数作为其定时时间到后的回调函数,如:

初始化语句放在哪里呢?这里仅仅展示了使用软件定时器实现am_buzzer_beep_async()函数的范例,后文再介绍初始化软件定时器的合适时机。
由于am_buzzer_beep()和am_buzzer_beep_async()接口可以直接基于am_buzzer_on()和am_buzzer_off()实现,因此实现蜂鸣器接口的核心是实现am_buzzer_on()和am_buzzer_off()接口,按照LED 或HC595 的设计方法,可以抽象对应的两个方法。即:

虽然按照这种设计方法是完全可行的,但是考虑到on 和off 是一组相互对称的接口,功能是同属一类的,具有很大的相似性,因此,可以仅抽象一个方法,使用一个布尔类型的参数区分操作是打开还是关闭,比如:

可见,定义抽象方法并不一定是原封不动的按照接口定义抽象方法,可以作适当的调整,只要基于抽象方法,能够实现通用接口即可。
虽然只有一个抽象方法,但是为了保证结构的统一,也为了方便后续扩展(如新增抽象方法等),往往还是将抽象方法放到一个虚函数表中。即:

类似地,将抽象方法和p_cookie 定义在一起,即为抽象的蜂鸣器设备。如:

在前面实现am_buzzer_beep_async()接口时,使用到了软件定时器,显然,软件定时器是用于实现一个蜂鸣器鸣叫功能的,是与蜂鸣器设备相关的,其不应定义为全局变量,取而代之的是,直接定义在抽象设备结构体中,即:

抽象设备中定义的抽象方法需要由具体的蜂鸣器设备来完成,am_buzzer_on()和am_buzzer_off()接口则可以直接基于抽象方法实现。
在定义蜂鸣器接口时,由于蜂鸣器是单实例设备(系统中只有一个),因此没有在接口中定义区分蜂鸣器对象的参数,如ID 号或者句柄参数等,那么,在实现接口时,如何找到相应的设备呢?由于在系统中只有一个蜂鸣器设备,因此,可以直接使用一个全局变量来指向蜂鸣器设备,am_buzzer_on()和am_buzzer_off()的实现详见程序清单8.26。
程序清单8.26 am_buzzer_on 和am_buzzer_off ()的范例程序

其中,__gp_buzzer_dev 是指向蜂鸣器设备的指针雅安三雅,初始没有任何有效的蜂鸣器设备,因此初始值为NULL。显然,要正常使用蜂鸣器,就必须使__gp_buzzer_dev 指向有效的蜂鸣器设备,这就需要由具体蜂鸣器设备实现pfn_buzzer_set 抽象方法。
为了完成__gp_buzzer_dev 的赋值泰星om,需要定义一个设备注册接口,用于向系统中注册一个有效蜂鸣器设备:

其中,为了方便向系统中添加一个蜂鸣器设备时,避免直接操作蜂鸣器设备的各个成员,将需要赋值的成员通过参数传递给接口函数。其实现详见程序清单8.27。
程序清单8.27 向系统中添加蜂鸣器设备

该程序首先判定参数的有效性,然后完成了抽象设备中抽象方法和p_cookie 赋值,接着给全局变量__gp_buzzer_dev 的赋值,使其指向有效的蜂鸣器设备,最后男模丁一 ,初始化了抽象设备中的软件定时器,便于实现异步的蜂鸣器鸣叫接口,由此可见,软件定时器的初始化操作是在添加一个蜂鸣器设备时完成的。
显然,接下来,就需要基于抽象的蜂鸣器设备派生具体的蜂鸣器设备,在具体的蜂鸣器设备中,完成抽象方法pfn_buzzer_set 的实现,并使用am_buzzer_dev_register()接口向系统中添加一个蜂鸣器设备,使得用户可以使用蜂鸣器通用接口操作到具体有效的蜂鸣器。
为了便于查阅,如程序清单8.28 所示展示了蜂鸣器设备接口文件(am_buzzer_dev.h)的内容。其对应的类图详见图8.9。
程序清单8.28 am_buzzer_dev.h 文件内容


图8.9 抽象的蜂鸣器设备类
2. 具体的蜂鸣器设备类
以使用PWM 输出控制蜂鸣器发声为例,简述具体蜂鸣器设备的实现方法。首先应该基于抽象设备类派生一个具体的设备类,其类图详见图8.10,可直接定义具体的蜂鸣器设备类,如:


图8.10 具体的蜂鸣器设备类
am_buzzer_pwm_dev_t 即为具体的蜂鸣器设备类。具有该类型后,即可使用该类型定义一个具体的蜂鸣器设备实例,即:

特别地,由于蜂鸣器是单实例设备,不能够使用该类型定义多个实例,因此ca1835,可以直接在具体设备实现的文件内部定义一个蜂鸣器设备实例,无需用户使用该类型自定义设备实例。基于此,am_buzzer_pwm_dev_t 类型无需开放给用户,可以直接定义在.c 文件中,由于am_buzzer_pwm_dev_t 类型无需开放给用户,仅内部使用,因此可以修改类型名为双下划线“__”开头,如在am_buzzer_pwm.c 文件中定义设备类型以及对应的设备实例如下:

在使用PWM 输出控制蜂鸣器时,需要知道PWM 的句柄,通道号等相关信息,这些信息需要保存在设备中,因此更新设备类型的定义如下:

显然,这些成员需要初始化后才能使用,定义初始化函数的原型为:

其中,pwm_handle 为标准的PWM 服务句柄,chan 为PWM 通道号,duty_ns 和period_ns分别指定了输出PWM 波形的脉宽和周期,决定了蜂鸣器鸣叫的响度和频率,比如,AM824-Core 板载的蜂鸣器。
若使用SCT 输出PWM,姜柔伊由于PIO0_24 对应SCT 的通道1,因此初始话函数的调用形式如下:

程序中,使用了am_lpc82x_sct0_pwm_inst_init()函数获取到了PWM 句柄,使用了通道1,并设定输出PWM 的周期为400000ns,即 2.5KHz (1000000000 / 400000),脉宽恰好为周期的一半顺电网上商城,即输出PWM 的占空比为50%。初始化函数的实现范例详见程序清单8.29。
程序清单8.29 初始化函数实现范例

该程序首先判定了参数的有效性,然后完成了设备实例中相关成员的赋值,接着调用了am_buzzer_dev_register()函数,将蜂鸣器设备添加到系统中,最后配置了PWM 输出通道的脉宽和周期。添加设备时,将p_funcs 赋值为&__g_buzzer_pwm_drv_funcs,p_cookie 赋值为具体设备的地址,即p_cookie 指向了设备自身。__g_buzzer_pwm_drv_funcs 中包含了抽象方法的具体实现,完整定义详见程序清单8.30。
程序清单8.30 抽象方法的实现

为了便于查阅,如程序清单8.31 所示展示了蜂鸣器设备接口文件(am_buzzer_pwm.h)的内容。
程序清单8.31 am_buzzer_pwm.h 文件内容

由此可见,与其它具体设备的接口文件(详见程序清单8.14、程序清单8.17 和程序清单8.23)相比,不同的是,其没有包含具体设备类型的定义,初始化接口的第一个参数,也不是指向具体设备的指针。这是由于蜂鸣器是单实例设备,系统中最多只能定义一个,因此直接在实现文件的内部完成了设备实例的定义,相关类型无需开放给用户。同理,由于是单实例设备,初始化函数初始化的必然是文件内部定义的设备实例,无需额外使用指向设备的指针指定要初始化的设备。
至此,详细介绍了LED 通用接口、HC595 接口和蜂鸣器接口暗帝的禁宠,它们代表了AMetal 中典型的3 种类型的设备接口朱永博。
LED 通用接口:使用唯一ID 区分不同设备;
HC595 接口:使用句柄区分不同的设备,句柄本质上是指向设备的指针;
蜂鸣器接口:不使用任何参数区分不同设备,是一种单实例设备。
8.4 温度采集接口
>>> 8.4.1 定义接口
1. 接口命名
由于操作的对象是温度(temperature),为了缩短接口名,将temperature 缩写为temp,因此,接口命名以“am_temp_”作为前缀。对于温度采集,主要的操作就是读取当前温度。可定义接口名为:
am_temp_read
2. 接口参数
显然,一个系统中可能存在多个温度传感器,可以简单的使用句柄来区分不同的温度传感器,因此第一个参数的类型定义为温度传感器句柄,和HC595 设备类似,其应该定义为指向抽象温度设备的指针,假定抽象温度设备的类型为am_temp_dev_t,则handle 的类型可以定义为:

读取温度接口的核心功能是返回当前的温度值,首先需要定义温度值的类型,然后再确定温度值的返回方式:通过返回值返回或通过出口参数返回。
通常使用1 位小数表示温度值,比如,37.5℃,由此可见,温度值需要使用小数表示,但要求的精度并不高,往往只会精确到小数点后一位,因此温度值可以使用float 类型表示。
由于AMetal 运行的实际硬件平台往往是以低端的Cortex-M0、Cortex-M0+和Cortex-M3等作为内核的芯片,这些芯片没有硬件浮点运算单元,浮点运算的效率很低。因此,AMetal平台中,不建议使用浮点类型,据此,可以使用整数表示温度值,同时,为了保证一定的精度,使用整数表示扩大1000 倍后的温度值。如实际温度为37.5 度,则使用整数37500 表示。使用这种方法巧妙的避免了使用浮点类型,但也能保证实际温度的精度为小数点后三位。由于温度可能存在负值,因此,使用有符号的32 位数据来表示温度值,即温度值的类型定义为int32_t。
在通用接口中,返回值往往定义为int 类型的错误号,且使用负数表示出错,显然,如果使用返回值直接返回温度,用户将无法区分温度为负数和读取温度出错的情况。为此,使用一个输出参数,用以返回温度值,即定义一个int32_t 类型的指针作为输出参数:
am_temp_read(am_temp_handle_t handle, int32_t *p_temp)
其中,handle 为温度传感器的句柄,p_temp 为输出参数,用于返回当前的温度值,其表示的温度值为实际温度值的1000 倍。
3. 返回值
接口无特殊说明,直接将所有接口的返回值定义为int 类型的标准错误号。基于此,完整的读取温度接口的原型为:

其对应的类图详见图8.11。

图8.11 温度采集接口
>>> 8.4.2 实现接口
1. 抽象的温度采集设备类
根据读取温度接口,可以定义相应的抽象方法,并将其存放在一个虚函数表中:

类似地,将抽象方法和p_cookie 定义在一起,即为抽象的温度采集设备。比如:

显然,具体的温度采集设备直接从抽象的温度采集设备派生,然后由具体的温度采集设备根据实际的硬件,实现读取温度的抽象方法。
在读取温度接口中,使用了handle 作为第一个参数,其本质上是指向设备的指针,读取温度接口可以直接调用抽象方法实现,详见程序清单8.32。
程序清单8.32 读取温度接口实现

在接口实现中,没有与硬件相关的实现代码,仅仅是简单的调用了抽象方法。抽象方法需要由具体的温度采集设备来实现。类似地,由于读取温度接口的实现非常简单,往往将其实现直接以内联函数的形式存放在.h 文件中。
为便于查阅,如程序清单8.33 所示展示了抽象温度采集设备接口文件(am_temp.h)的内容,其包含了抽象温度采集设备相关的抽象方法定义、类型定义和接口实现,对应的类图详见图8.12。

图8.12 抽象的温度采集设备类
程序清单8.33 am_temp.h 文件内容

2. 具体的温度采集设备类
以使用LM75B 温度传感器实现温度采集为例,简述具体温度采集设备的实现方法。首先应该基于抽象设备类派生一个具体的设备类,其类图详见图8.13,可直接定义具体的温度采集设备类。比如:


图8.13 具体的温度采集设备类
am_temp_lm75_t 为具体的温度采集设备类,具有该类型后,即可使用该类型定义一个具体的温度采集设备实例:

LM75B 是标准的I2C 从机器件,需要知道LM75B 的从机地址,才能使用I2C 总线读取LM75B 中的温度数据。由于从机地址与LM75 外部引脚电平相关,因此LM75 的地址信息需要由用户根据实际硬件电路设置。将需要由用户提供的设备相关信息存放到一个新的设备信息结构体类型中。比如:

当使用AM824-Core 上板载的LM75B 时,LM75B 的7 位I2C 从机地址为1001A2A1A0,由于A0、A1、A2 均与地连接为低电平,因此可得板载LM75B 的7 位从机地址为1001000,即:0x48。基于此,板载LM75B 对应的设备实例信息可以定义如下:

同理,在设备类中需要维持一个指向设备信息的指针。此外,由于使用I2C 接口从LM75B中读取温度数据时双偷盗宝,LM75B 相当于是一个I2C 从设备,为了使用I2C 接口与之通信,需要为LM75B 定义一个与之对应的I2C 从设备,新增两个成员,完整的温度采集设备定义即为:

显然,在使用I2C 接口从LM75B 中读取温度之前,需要完成设备中各成员的赋值,这些工作通常在驱动的初始化函数中完成,定义初始化函数的原型为:

p_lm75 为指向am_temp_lm75_t 类型实例的指针;
p_devinfo 为指向am_temp_lm75_info_t 类型实例信息的指针。
handle 为I2C 句柄,便于使用I2C 接口读取温度数据,初始化函数的返回值即为温度采集设备句柄,其调用形式如下:

返回值即为温度采集设备的句柄,可以作为温度采集接口的第一个参数(handle)的实参乔乔纳斯 ,初始化函数的实现范例详见程序清单8.34陶醉哥 。
程序清单8.34 初始化函数实现范例

该程序首先建立了标准的I2C 从设备,便于后续使用I2C 接口读取数据,然后初始化了p_info 成员,接着完成了抽象温度采集设备中p_funcs 和p_cookie 的赋值,最后返回设备地址作为用户操作温度采集设备的句柄。pfuncs 赋值为了&__g_temp_lm75_drv_funcs,其中包含了读取温度抽象方法的具体实现,完整定义详见程序清单8.35。
程序清单8.35 抽象方法的实现

在读取温度的实现函数__temp_lm75_read()中,首先使用I2C 接口从LM75B 中读取出当前的实际温度值,详见程序清单8.35(6);接着对数据进行简单处理,两字节数据整合为一个16 位有符号的温度值temp,详见程序清单8.35(10 ~ 11);最后,确认p_temp 指针有效后,将temp 乘以125,再除以32,最终的结果作为输出的温度值。
为什么将temp 乘以125,然后再除以32 呢?这是因为LM75B 中直接读取的数据时实际温度值的256倍,即:实际温度= temp / 256。
而温度采集接口需要返回的温度值是实际温度的1000 倍,即:
* p _ temp=实际温度*1000 = temp/ 256*1000 = temp*1000/ 256
化简可得:

为了便于查阅兽人之脔宠,如程序清单8.36 所示展示了具体温度采集设备(LM75B)接口文件(am_temp_lm75.h)的内容。
程序清单8.36 am_temp_lm75.h 文件内容

在公众号后台回复关键字【编程】,即可在线阅读《面向AMetal框架与接口的编程(上)》,回复【程序设计】,即可在线阅读《程序设计与数据结构》。
延伸阅读
周立功:深入理解AMetal——HC595 接口
周立功:深入理解AMetal——LED 通用接口
周立功:面向通用接口的编程——数码管与矩阵键盘联合使用、(ZLG72128)数码管与键盘管理、温控器
周立功:面向通用接口的编程——数码管
周立功:面向通用接口的编程——温度采集接口、键盘
周立功:面向通用接口的编程——LED 控制接口、HC595 接口、蜂鸣器控制接口
周立功:重用外设驱动代码——键盘与数码管接口
周立功:重用外设驱动代码——读写卡模块
周立功:重用外设驱动代码—— RTC 实时时钟(2)
周立功:重用外设驱动代码—— RTC 实时时钟(1)
周立功:重用外设驱动代码——SPI NOR Flash 存储器
周立功:重用外设驱动代码——EEPROM 存储器
周立功:深入浅出AMetal—— UART 总线
周立功:深入浅出AMetal——A/D 转换器
周立功:深入浅出AMetal—— SPI 总线和 IIC 总线
周立功:深入浅出AMetal——键盘扫描接口和 PWM 接口
周立功:深入浅出AMetal——LED 数码管接口
周立功:深入浅出AMetal——接口与实现
周立功:面向接口的编程——SPI 总线和IIC 总线
周立功:面向接口的编程——事件驱动和键盘管理
周立功:面向接口的编程——LED 数码管
周立功:面向接口的编程——平台技术、开关量信号
周立功:PWM 实现DAC 电路设计
周立功:ADC 信号调理电路设计必要措施、实测验证和应用说明
周立功:ADC 信号调理电路设计应用背景和电路设计
周立功:MicroPort 模块介绍和MiniPort 模块说明
周立功:开关矩阵(SWM)和 AM824-Core的应用
周立功:认识 LPC824 和 LPC84x 微控制器
周立功单片机∣一个有用的公众号长按,识别二维码,加关注
作者:admin 分类:全部文章 浏览:68