第五章为深入浅出AMetal,本文内容为5.5 SPI 总线和5.6 I2C 总线。
5.5 SPI 总线
>>> 5.5.1 初始化
在使用SPI 通用接口前,必须先完成SPI 的初始化,以获取标准的SPI 实例句柄。LPC82x支持SPI 功能的外设有SPI0 和SPI1,为方便用户使用,AMetal 提供了与各外设对应的实例初始化函数,详见表5.10。
表5.10 SPI 实例初始化函数(am_lpc82x_inst_init.h)
这些函数的返回值均为am_spi_handle_t 类型的SPI 实例句柄,该句柄将作为SPI 通用接口中handle 参数的实参。类型am_spi_handle_t(am_spi.h)定义如下:
因为函数返回的SPI 实例句柄仅作为参数传递给SPI 通用接口,不需要对该句柄做其它任何操作,因此完全不需要了解该类型。注意,若函数返回的实例句柄的值为NULL,则表明初始化失败,不能使用该实例句柄。
如需使用SPI0,则直接调用SPI0 实例初始化函数,即可获取对应的实例句柄:
>>>5.5.2 接口函数
MCU 的SPI 主要用于主从机的通信,AMetal 提供了8 个接口函数,详见表5.11。
表5.11 SPI 标准接口函数
1. 从机实例初始化
对于用户来说,使用SPI 往往是直接操作一个从机器件,MCU 作为SPI 主机,为了与从机器件通信,需要知道从机器件的相关信息,比如,SPI 模式、SPI 速率、数据位宽等。这就需要定义一个与从机器件对应的实例(从机实例),并使用相关信息完成对从机实例的初始化。其函数原型为:
p_dev 是指向SPI 从机实例描述符的指针,am_spi_device_t 在am_spi.h 文件中定义:
该类型用于定义从机实例,用户无需知道其定义的具体内容,只需要使用该类型定义一个从机实例。即:
mode 指定使用的模式,SPI 协议定义了4 种模式,详见表5.12。各种模式的主要区别在于空闲时钟极性(CPOL)和时钟相位选择(CPHA)的不同。CPOL 和CPHA 均有两种选择,因此两两组合可以构成4 种不同的模式,即模式0~3。当CPOL 为0 时,表示时钟空闲时,时钟线为低电平,反之,空闲时为高电平;当CPHA 为0 时,表示数据在第1 个时钟边沿采样,反之,则表示数据在第2 个时钟边沿采样。
表5.12 SPI 常用模式标志
cs_pin 和pfunc_cs 均与片选引脚相关。pfunc_cs 是指向自定义片选控制函数的指针,若pfunc_cs 的值为NULL,驱动将自动控制由cs_pin 指定的引脚实现片选控制;若pfunc_cs 的值不为NULL,指向了有效的自定义片选控制函数,则cs_pin 不再被使用,片选控制将完全由应用实现。当需要片选引脚有效时,驱动将自动调用pfunc_cs指向的函数,并传递state 的值为1。当需要片选引脚无效时,也会调用pfunc_cs 指向的函数,并传递state的值为0。一般情况下,片选引脚自动控制即可,即设置pfunc_cs 的值为NULL,cs_pin 为片选引脚,如PIO0_13。使用范例详见程序清单5.60。
程序清单5.60 am_spi_mkdev()范例程序
2. 设置从机实例
设置SPI 从机实例时,会检查MCU 的SPI 主机是否支持从机实例的相关参数和模式。如果不能支持,则设置失败,说明该从机不能使用。其函数原型为:
其中的p_dev 是指向SPI 从机实例描述符的指针,如果返回AM_OK,说明设置成功;如果返回-AM_ENOTSUP,说明设置失败,不支持的位宽、模式等,详见程序清单5.61。
程序清单5.61 am_spi_setup()范例程序
3. 传输初始化
在AMetal 中,将收发一次数据的过程抽象为一个“传输”的概念,要完成一次数据传输,首先就需要初始化一个传输结构体,指定该次数据传输的相关信息。其函数原型为:
其中,p_trans 为指向SPI 传输结构体的指针,am_spi_transfer_t 类型是在am_spi.h 中定义的。即:
在实际使用时,只需要定义一个该类型的传输结构体即可。比如:
表5.13 传输特殊控制标志
因为SPI 是全双工通信协议,所以单次传输过程中同时包含了数据的发送和接收。函数的参数中,p_txbuf 指定了发送数据的缓冲区,p_rxbuf 指定了接收数据的缓冲区,nbytes 指定了传输的字节数。特别地,有时候可能只希望单向传输数据,若只发送数据,则可以设置p_rxbuf 为NULL;若只接收数据,则可以设置p_txbuf 为NULL。
当传输正常进行时,片选会置为有效状态,cs_change 的值将影响片选何时被置为无效状态。若cs_change 的值为0,表明不影响片选,此时,仅当该次传输是消息(多次传输组成一个消息,消息的概念后文会介绍)的最后一次传输时,片选才会被置为无效状态。若cs_change 的值为1,表明影响片选,此时,若该次传输不是消息的最后一次传输,则在本次传输结束后会立即将片选设置为无效状态,若该次传输是消息的最后一次传输,则不会立即设置片选无效,而是保持有效直到下一个消息的第一次传输开始,详见程序清单5.62。
程序清单5.62 am_spi_mktrans()范例程序
4. 消息初始化
一般来说,与实际的SPI 器件通信时,往往采用的是“命令”+“数据”的格式,这就需要两次传输:一次传输命令,一次传输数据。为此,AMetal 提出了“消息”的概念,一个消息的处理即为一次有实际意义的SPI 通信,其间可能包含一次或多次传输。
一次消息处理中可能包含很多次的传输,耗时可能较长,为避免阻塞,消息的处理采用异步方式。这就要求指定一个完成回调函数,当消息处理完毕时,自动调用回调函数以通知用户消息处理完毕。回调函数的指定在初始化函数中完成,初始化函数的原型为:
其中的p_msg 为指向SPI 消息结构体的指针,am_spi_message_t 类型是在am_spi.h 中定义的。即:
实际使用时,仅需使用该类型定义一个消息结构体。即:
pfn_callback 指向的是消息处理完成回调函数,当消息处理完毕时,将调用指针指向的函数。其类型am_pfnvoid_t 在am_types.h 中定义的。即:
由此可见,函数指针指向的是参数为void *类型的无返回值函数。驱动调用回调函数时,传递给该回调函数的void*类型的参数即为p_arg 的设定值,详见程序清单5.63。
程序清单5.63 am_spi_msg_init()范例程序
5. 在消息中添加传输
一次消息处理中包含单次或多次的传输,在消息处理前,需要将消息和相关的传输关联起来。该函数用于添加一个传输至消息中,其函数原型为:
其中,p_msg 指向am_spi_msg_init()初始化的消息,p_trans 指向am_spi_mktrans()初始化的传输。可以多次使用该函数以便向一个消息中添加多个传输,由于每次都将传输添加在消息的尾部,因此先添加的传输先处理,后添加的传输后处理,详见程序清单5.64。
程序清单5.64 am_spi_trans_add_tail()范例程序
6. 启动SPI 消息处理
该函数用于启动消息的处理,其函数原型为:
其中,p_dev 为指向SPI 从机实例描述符的指针,用于指定本次消息处理中收发数据的从机对象;p_msg 为指向本次需要处理的SPI 消息结构体的指针。如果返回AM_OK,说明启动成功,当消息中所有的传输依次处理完毕时,将调用初始化消息时指定的处理完毕回调函数;如果返回-AM_EINVAL,说明因参数错误启动失败,详见程序清单5.65。
程序清单5.65 am_spi_msg_start ()范例程序
在这里,定义了一个初始值为0 的变量complete_flag,在初始化消息时,将它的地址作为回调函数的参数,因此在回调函数中,p_arg 就是指向complete_flag 的指针,可以通过该指针将complete_flag 的值修改为1,如果检测complete_flag 的值为1,表明消息处理完成。
7. 先写后读
SPI 传输和SPI 消息实现数据的发送和接收使得SPI 的使用非常灵活,可以支持丰富的SPI 从机器件。但正因为其灵活性,使得接口较多,使用起来较为繁琐。对于绝大部分SPI从机器件,并不需要如此灵活,只需要实现简单的数据发送和接收就可以了,基于此,AMetal提供了两种十分常用的情形:写入一段数据后读取一段数据(先写后读);写入一段数据后再写入一段数据(连续两次写)。
先写后读即是主机先发送数据至从机(写),再自从机接收数据(读)。注意,该函数会等待数据传输完成后才会返回,因此该函数是阻塞式的,不应在中断环境中调用。其函数原型为:
如果返回AM_OK,说明数据写和读成功完成;如果返回-AM_EINVAL,说明由于参数错误导致数据写和读失败;如果返回-AM_EIO,说明在数据写或读的过程中发生错误,详见程序清单5.66。
程序清单5.66 am_spi_write_then_read()范例程序
8. 连续两次写
连续两次写即是主机先发送缓冲区0 的数据至从机(写),再发送缓冲区1 的数据至从机(写)。如果只需要发送一次数据,可以将第二次发送的数据缓冲区设置为NULL,并设置发送长度n_tx1 为0。值得注意的是,该函数同样是阻塞式的,会等待两次数据发送完成后才会返回,不应在中断环境中调用。其函数原型为:
如果返回AM_OK,说明消息处理成功;如果返回-AM_EINVAL,说明参数错误导致数据发送失败;如果返回-AM_EIO,说明在发送数据的过程中发生错误,详见程序清单5.67。
程序清单5.67 am_spi_write_then_write()范例程序
>>> 5.5.3 SPI 扩展接口
LPC824 与74HC595 的硬件连接详见表5.14,当74HC595 作为SPI 从机时,数据仅需从MCU主机传送至SPI 从机,无需读取数据。对于MCU主机,数据是单向传输,只有数据输出而没有输入,因此无需使用MISO。显然,扩充74HC595仅占用MCU 的SCK、MOSI 与SSEL 引脚,即可实现8 位数据的输出。
表5.14 74HC595 硬件连接
在使用SPI 驱动74HC595 时,必须先调用am_spi_mkdev()初始化与74HC595 对应的SPI从机实例。为此需要获取到数据宽度、SPI 模式、最高时钟频率和片选引脚等信息。即:
-
数据宽度:74HC595 只有8 个并行输出口,因此每次传输的数据宽度为8 位。
-
SPI 模式:8 位数据是在CP 时钟信号上升沿作用下依次送入74HC595 的,因此在空闲时对时钟没有要求。如果选择空闲时钟极性为低电平(CPOL=0),则必须在第一个时钟边沿(上升沿)采样数据(CPHA=0),即模式0。反之,如果选择空闲时钟极性为高电平(CPOL=1),则必须在第二个时钟边沿(上升沿)采样数据(CPHA=1),即模式3。因此选择模式0 和模式3 均可,后续的程序选择模式3 作为范例。
-
最高时钟频率:虽然74HC595 最高时钟频率高达100MHz,但MCU 最高主频只有30MHz,因此最高时钟频率设置为一个相对合理的范围,比如,3000000Hz(3MHz)。
-
片选引脚:片选引脚为PIO0_13。
有了这些信息,即可配置与74HC595 对应的SPI 从机实例,详见程序清单5.68。
程序清单5.68 初始化与74HC595 对应的从机实例范例程序
接下来,可以使用消息的方式或者am_spi_write_then_write()和am_spi_write_then_read()进行数据的发送与接收。由于使用消息的方式进行数据发送时参数较多,因此暂不使用消息的方式。因为MCU 不需要从74HC595 中读取数据,所以直接使用am_spi_write_then_write()进行数据发送,详见程序清单5.69。
程序清单5.69 驱动74HC595 输出的范例程序
为了方便后续使用,不妨封装74HC595 的驱动程序,这样就可以将74HC595 当作8 位I/O 口扩展接口来使用了。显然,需要为74HC595 编写初始化接口和数据输出接口,其分别用于初始化与74HC595 对应的SPI 从机实例和用于输出指定的8 位数,函数的声明和实现详见程序清单5.70 所示的hc595.h 和程序清单5.71 所示的hc595.c。
程序清单5.70 hc595.h 接口文件
程序清单5.71 hc595.c 实现文件
基于74HC595 的数据发送函数,可以实现使用74HC595 驱动数码管显示,以节省引脚。同I/O 驱动数码管一致,使用软件定时器实现数码管的自动扫描,详见程序清单5.72。
程序清单5.72 新增使用软件定时器相关函数
同样地,将新的代码添加到digitron1.c 中,其相应的函数接口添加到程序清单4.47 所示的digitron1.h 中,详见程序清单5.73。
程序清单5.73 digitron1.h 文件内容
显然,程序与之前的代码基本相同,仅仅是回调函数调用的扫描函数变化了,测试程序详见程序清单5.74。
程序清单5.74 测试软件定时器自动扫描
由于回调函数运行在中断环境,而am_spi_write_then_write()要等到数据发送完毕后才会返回,因此是阻塞式的,所以调用该函数实现的hc595_send_data()不能在中断环境中使用。为了实现非阻塞式的(异步)数据发送函数,可以使用SPI 消息的方式实现发送数据。为了区分与之前的hc595_send_data(),将其命名为hc595_send_data_async(),详见程序清单5.75。
程序清单5.75 hc595_send_data_async()函数范例
在发送数据时,要先将数据data 保存g_tx_buf 中。因为使用SPI 消息的方式发送数据时,函数是异步的,会立即返回,函数返回后,因data 是局部变量,其地址空间就被释放了。驱动获取需要发送的数据时,是在缓冲区表明的地址中取数据,因此必须保证缓冲区在整个数据传输过程中都是有效的。这里使用了一个全局变量来保存数据,使得缓冲区一直有效。为什么使用am_spi_write_then_write()函数不需要这样做呢?因为这个函数是同步的,会等到数据发送完毕后才返回,在整个数据传输过程中,data 的地址是有效的,不会被释放。
这样的异步传输函数可行吗?如果使用者以较长的时间间隔来调用该函数,每次调用前,上一个数据传输都已经正确完成,则可以正常进行数据发送,不会出现问题。但是如果时间间隔很短,比如,连续2 次调用了该函数分别发送两个数据,将导致上一个transfer 被覆盖,造成一种严重错误。可以类似SPI 消息一样增加一个回调函数,当数据发送完成后,调用回调函数通知用户数据发送完毕。由于消息本身就有这一特性,因此只需要直接将用户传递的回调函数作为SPI 消息初始化的回调函数参数即可,详见程序清单5.76。
程序清单5.76 修改hc595_send_data_async()函数
为了便于后续使用,则将该函数的声明存放到程序清单5.70 所示的hc595.h 中,详见程序清单5.77。
程序清单5.77 hc595.h 文件内容
实现该异步数据发送函数后,即可实现在中断环境中发送数据。显然,使用软件定时器实现数码管自动扫描需要修改digitron_hc595_disp_scan(),使其调用hc595_send_data_async()来实现扫描。不妨使用一个标志位,当标志位置1 时,说明传输完成,详见程序清单5.78。
程序清单5.78 修改digitron_hc595_disp_scan()函数(1)
程序使用g_flag 变量来标志消影段码是否传输结束,初看起来并没有什么问题,这是一种通用的编程方法。但的确犯了一个很严重的错误,由于该函数直接使用了阻塞式while()循环等待语句,虽然hc595_send_data_async()是异步的,但加上等待语句后,又将扫描函数
变成阻塞式的了,因此该扫描函数还是无法在中断环境中使用。
扫描一次数码管,首先需要传送消影段码(0xFF),接着确定相应的位选,然后传送显示段码,即会在极短的时间内调用2 次段码传送函数(消影段码和显示的段码)。显然,消影段码没有传送完毕不能传送显示段码,由于消影段码传送完毕后会调用回调函数,为何不将后续代码放到消影段码传送完成的回调函数中执行呢?详见程序清单5.79。
程序清单5.79 修改digitron_hc595_disp_scan()函数(2)
程序只是将之前的扫描函数分成了两部分,将消影段码后的内容放到了回调函数中实现,解决了等待消影段码传送完毕的问题。那么后续发送正常显示的段码,还需要等待其结束吗?其实在正常显示的段码传送完成后,并不需要再做其它操作,因此可以不用设置回调函数。如果不利用回调函数判断其是否传送完毕,那再次扫描时,是否会因上次消息处理还未完成
而产生错误呢?下次扫描是在5ms 之后,由于SPI 传输速率很快,3MHz 的速率传输8 位数据只需要几微秒,因此5ms 的时间足以使其传输完毕,因此能够确保正常显示的段码传送在下一次传输数据前成功完成。修改后的使用软件定时器实现自动扫描的函数,由于接口并没有改变,只是修改了实现,因此可以直接使用程序清单5.74 的范例程序来进行测试。
5.6 I2C 总线
绝大部分情况下,MCU 都作为I2C 主机与I2C 从机器件通信,因此这里仅介绍AMetal中与I2C 主机相关的接口函数。
>>> 5.6.1 初始化
在使用I2C 通用接口传输数据前,必须先完成I2C 的初始化,便于获取I2C 实例句柄。在LPC824 中,支持I2C 功能的外设有:I2C0、II2C1、I2C2 和I2C3,各I2C 外设都提供了对应的实例初始化函数,详见表5.15。
表5.15 I2C 实例初始化函数(am_lpc82x_inst_init.h)
这些函数返回值均为am_i2c_handle_t 类型的I2C 实例句柄,该句柄将作为I2C 通用接口中handle 参数的实参。类型am_i2c_handle_t(am_i2c.h)定义如下:
因为函数返回的I2C 实例句柄仅作为参数传递给I2C 通用接口,不需要对该句柄作其它任何操作,因此完全不需要对该类型作任何了解。注意,如果函数返回的实例句柄的值为NULL,表明初始化失败,该实例句柄不能被使用。
如果使用I2C0,则直接调用I2C0 实例初始化函,即可获取对应的实例句柄:
>>> 5.6.2 接口函数
在AMetal 中,MCU 作为I2C主机与I2C从机器件通信的相关接口函数详见表5.16。
表5.16 I2C标准接口函数
1. 从机实例初始化
对于用户来讲,使用I2C的目的就是直接操作一个从机器件,比如,LM75、E2PROM等。MCU 作为I2C主机与从机器件通信,需要知道从机器件的相关信息,比如,I2C从机地址等。这就需要定义一个与从机器件对应的实例,即从机实例,并使用相关信息完成对从机实例的初始化。从机实例初始化函数的原型为:
其中,p_dev 为指向am_i2c_device_t 类型(am_i2c.h)I2C 从机实例的指针,该类型定义如下:
使用时无需知道该类型定义的具体内容,仅需使用该类型完成一个I2C 从机实例的定义:
其中,dev 为用户自定义的从机实例,其地址作为p_dev 的实参传递。dev_flags 为从机实例的相关属性标志,可分为3 大类:从机地址的位数、是否忽略无应答和器件内子地址(通常又称之为“寄存器地址”)的字节数。具体可用属性标志详见表5.17,可使用“|”操作连接多个属性标志。
表5.17 从机设备属性
使用范例详见程序清单5.80。
程序清单5.80 am_i2c_mkdev()范例程序
2. 写操作
向I2C 从机实例指定的子地址中写入数据的函数原型为:
如果返回值为AM_OK,表明写入数据成功;如果返回值为其它值,表明写入数据失败。范例程序详见程序清单5.81。
程序清单5.81 am_i2c_write()使用范例
3. 读操作
从I2C从机实例指定的子地址中读出数据的函数原型为:
如果返回值为AM_OK,表明读取数据成功;如果返回值为其它值,表明读取数据失败,其相应的范例程序详见程序清单5.82。
程序清单5.82 am_i2c_read()使用范例
>>> 5.6.3 I2C扩展接口
在使用am_i2c_read()函数前,需要先使用am_i2c_mkdev()初始化与LM75B 对应的I2C从机实例,便于LPC824 读取温度值。初始化从机实例时,还需要知道两个重要的信息:器件从机地址和实例属性。
LM75B 的从机地址为7 位,1001xxx,其中地址位0~2 分别与硬件连接的A0~A2 一一对应。由于A0~A2 均与地连接,因此xxx 的值均为0,LM75B 的从机地址为0x48。
实例属性可分为从机地址属性、应答属性和器件内子地址属性,LM75B 的从机地址为7 位,其对应的属性标志为AM_I2C_ADDR_7BIT。如果从机实例不能应答,则设置AM_I2C_IGNORE_NAK 标志。一般来说,标准的I2C从机实例可产生应答信号,除非特殊说明,否则都不需要该标志。
LM75B 共计有4 个寄存器,详见表5.18。由于寄存器的地址都为8 位,因此器件内子地址为一个字节,对应的属性标志为:AM_I2C_SUBADDR_1BYTE。由于只有一个字节,所以没有高字节与低字节之分, 也就不需要AM_I2C_SUBADDR_MSB_FIRST 或AM_I2C_SUBADDR_LSB_FIRST 标志。
表5.18 寄存器功能
使用am_i2c_mkdev()初始化一个LM75 从机实例的示例代码详见程序清单5.83。
程序清单5.83 初始化一个与LM75 对应的I2C 从机实例
初始化从机实例后,即可使用am_i2c_read()读取温度值,由表5.18 可知,温度值存于地址为0x00 的寄存器中,包含了两个字节的温度值,且是只读的。因此,可以直接使用am_i2c_read()读取子地址为0x00 的两字节内容,即温度值,使用范例详见程序清单5.84。
程序清单5.84 读取温度值
读取的这两字节数据表示的温度值是多少呢?这两个字节具体表示的温度值的含义可从芯片的数据手册获取。温度是以双字节16 位二进制补码方式表示的,分别保存在字节0和字节1 中,首先读出的是字节0 的数据。字节0 中保存了温度的整数部分,字节1 中保存温度的小数部分,仅高3 位有效,因此温度的分辨率为1/23 = 0.125℃。
如果将字节0 和字节1 合并为一个16 位有符号整数的话,则这个16 位有符号整数便是实际温度的256(28)倍。如果系统支持浮点数,则使用以下公式即可获得当前温度值:
当前温度值(浮点数变量)= (字节0 的值×28 + 字节1 的值)/ 256.0
在没有硬件浮点运算单元的MCU 中,这样的公式在计算时效率是非常低的。在实际使用过程中,一般也并不需要得出浮点数的温度值,仅仅在使用时稍加处理即可。比如,对于数码管显示温度值,只需要分别显示温度值的整数部分(使用整数表示)和小数部分(使用整数表示)即可,并不需要计算出浮点数。LM75 的接口函数声明详见程序清单5.85。
程序清单5.85 LM75 接口(lm75.h)
其中,lm75_read()的作用是读取LM75 温度值,其返回值(16 位有符号数)为实际温度的256 倍,其相应的实现(lm75.c)详见程序清单5.86。
程序清单5.86 LM75 接口的实现(lm75.c)
-
总线
+关注
关注
10文章
2856浏览量
87888 -
周立功
+关注
关注
38文章
130浏览量
37550
原文标题:周立功:深入浅出AMetal—— SPI 总线和 IIC 总线
文章出处:【微信号:Zlgmcu7890,微信公众号:周立功单片机】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论