3. 用户态 SPI 编程
Linux 的 SPI 总线设备文件名通常为/dev/spidevN.P( N=0, 1, 2..., P=0, 1, 2...),
其中 N 表示第几路 SPI 总线,而 P 表示在该路 SPI 总线中使用哪个 CS 信号线。
EasyARM-i.MX283A 提供了 1 路 SPI 总线,在该总线中只有 1 个 CS 信号线,其设备文
件名为/dev/spidev1.0。
root@EasyARM-iMX283 /# ls /dev/spi*
/dev/spidev1.0
3.1 SPI 编程接口
1. 打开设备
在使用 SPI 设备时,需要调用 open()函数打开设备文件,获得文件描述符,如程序清单3.1 所示。
程序清单 3.1 打开 SPI 设备文件
fd = open(―/dev/spidev1.0‖, O_RDWR);
if (fd < 0) {
perror( "can not open SPI device\n" );
}
2. 关闭设备
设备使用完成后,调用 close()函数关闭设备,如下所示:
close(fd);
3. 总线控制
通过调用 ioctl()函数使用不同的命令,应用程序可以配置 SPI 总线的极性和相位、设置
总线速率、数据字长度以及实现数据收/发。
(1) 设置总线极性和相位
SPI 总线极性及相位设置是通过 SPI_IOC_WR_MODE 命令实现的, 该命令的用法参考表 3.1。
表 3.1 SPI_IOC_WR_MODE 命令
---------------------------------------------------------
命 令 SPI_IOC_WR_MODE
调用方式 ret = ioctl(fd, SPI_IOC_WR_MODE, &mode);
功能描述 设置 SPI 总线的极性和相位
输入参数说明
mode 的可选值为: SPI_MODE_0、SPI_MODE_1、SPI_MODE_2、SPI_MODE_3,
这些值的说明参考下面内容。
返回值说明
0:设置成功
1:设置不成功
SPI_MODE_0 定义的模式为 POLARITY( 极性) =0、 PHASE( 相位) =0。
SPI_MODE_1 定义的模式为 POLARITY=0、 PHASE=1。
SPI_MODE_2 定义的模式为 POLARITY=1、 PHASE=0。
SPI_MODE_3 定义的模式为 POLARITY=1、 PHASE=1。
设置 SPI 总线极性和相位为 SPI_MODE_0 模式的方法可以参考如程序清单 3.2 所示的代码。
程序清单 3.2 设置 SPI 总线极性和相位示例
int mode = SPI_MODE_0;
ret = ioctl(fd_spi, SPI_IOC_WR_MODE, &mode);
if (ret == -1) {
printf("can't set wr spi mode\n");
return -1;
}
(2)设置每字的数据位长度
设置 SPI 总线上每字的数据位长度是使用 SPI_IOC_WR_BITS_PER_WORD 命令实现,
该命令的用法参考表 3.2。
表 3.2 SPI_IOC_WR_BITS_PER_WORD 命令
---------------------------------------------------------------------
命 令 SPI_IOC_WR_BITS_PER_WORD
调用方式 ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
功能描述 设置 SPI 总线上每字的数据位长度
输入参数说明 bits 为每字的二制位数,取值
返回值说明 0 为成功,其它值为失败
设置 SPI 总线的每字数据位长为 8 位的方法可以参考如程序清单 3.3所示的代码。
ret = ioctl(fd_spi, SPI_IOC_WR_BITS_PER_WORD, &bits); /* 设置 SPI 的数据位 */
if (ret == -1) {
printf("can't set bits per word\n");
return -1;
}
(3) 设置最大总线速率
设置 SPI 总线的最大速率是通过使用 SPI_IOC_WR_MAX_SPEED_HZ 命令实现,该命
令用法参考表 3.3。
表 3.3 SPI_IOC_WR_MAX_SPEED_HZ
--------------------------------
命 令 SPI_IOC_WR_MAX_SPEED_HZ
调用方式 ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
功能描述 设置 SPI 总线的最大速率
输入参数说明 speed 为需要设置的 SPI 总线的最大频率, 单位为 Hz
返回值说明 恒为 0:设置成功
注意:SPI 总线的最大速率设置后,在使用过程并不是只能使用该频率收/发数据,而仅仅约束
收/发数据时的最大频率。
(4) 数据接收/发送命令
在 SPI 总线实现数据收/发是使用 SPI_IOC_MESSAGE(n)命令实现,该命令用法参考表3.4。
表 3.4 SPI_IOC_MESSAGE(n)命令
------------------------------------------------------------------------------
命 令 SPI_IOC_MESSAGE(n)
调用方式 ret = ioctl(fd, SPI_IOC_MESSAGE(n), &tr);
功能描述 实现在 SPI 总线接收/发送数据操作,其中 n 的值可变
输入/输出参数说明
struct spi_ioc_transfer 结构体用于封装要收/发的数据。 tr 参数指定向 struct
spi_ioc_transfer 结构体的数组,数组长度为 n。
返回值说明
0:操作成功
1:操作失败
使用SPI_IOC_MESSAGE(n)命令收/发的数据都需要使用 struct spi_ioc_transfer结构体封
装,该结构体的定义如程序清单 3.4 所示。
程序清单 3.4 struct spi_ioc_transfer 结构体的定义
struct spi_ioc_transfer {
__u64 tx_buf; /* 指向发送数据的缓冲区 */
__u64 rx_buf; /* 指向接收数据的缓冲区 */
__u32 len; /* 收/发缓冲区中数据的长度 */
__u32 speed_hz; /* 总线速率 */
__u16 delay_usecs;
__u8 bits_per_word; /* 收/发数据的二进制位数 */
__u8 cs_change;
__u32 pad;
}
speed_hz 不能大于在 SPI_IOC_WR_MAX_SPEED_HZ 命令中设置的总线速率。
注意:
由于 iMX28xx 处理器的 SPI 控制器只支持半双工,因此 struct spi_ioc_transfer 结构体中
的 tx_buf 和 rx_buf 只能设置一个有效, 另一个必须设置为 0, 否则调用 ioctl 时会返回非零
值提示操作错误。
3.2 编程范例
1. 电路原理
在 AP-283Demo 板上,通过 74HC595 作为 SPI 从机器件驱动数码管。
硬件原理图
在 74HC595 芯片中,如果要将 8 位串行输入数据并行输出到 QA、 QB、 QC、 QD、 QE、
QF、 QG、 QH,则需要满足以下条件:
? 首先必须保证在 SCK 引脚输入连续的时钟信号;
? 在 SCK 引脚输入信号的上升沿,在 SI 引脚输入的数据被送入 QA 的第 1 级移位寄
存器, QA 移位寄存器原有的值移入 QB 移位寄存器, QB 移位寄存器原有的值移
入 QC 移位寄存器,以此类推;
? 在 RCK 引脚输入信号的上升沿,移位寄存器中的数据被送入锁存器;
? 若 OE 引脚输入低电平,则锁存器的值将在 QA~QH 引脚输出。
在 AP-283Demo 板上, MCU 的 SPI 接口控制 2 片 74HC595 带锁存的移位寄存器驱动 2
个共阴式的 LN3461BS 数码管,其中 U4 控制 8 位数据管的位选位, U6 控制 4 位数码管的
段选位,也就是说只要给数码管的位选位输送低电平,给数码管的段选位输送高电平,即可
点亮数码管。
MCU 作为主机通过 SPI 总线发送数据, 74HC595 作为人机接收数据,采用级联的方式
对 2 片 74HC595 进行操作,其数据的传递方式如下:
(1) 发送 8 位“位选”数据,且被保存在 U6 的移位寄存器中;
(2) 紧接再发送“段选”数据时,刚才发送的“位选”数据将通过级联方式移位到 U4
的移位寄存器中,后发送的“段选”数据则被保存在 U6 的移位寄存器吕;
(3) 当数据移位完成后,在 RCK 产生一个上升沿将移位寄存器中的数据移位到锁存器;
(4) 由于 OE 为低电平,锁存器的数据送到 U4、 U6 的 QA~QH 数据引脚上。
其中 U4、 U6 的 RCK 引脚连接到 i.MX283 处理器的 GPIO3.21 引脚。
2. 示例程序代码
在应用程序通过 SPI 总线控制数码管的示例程序代码如程序清单 3.5 所示。该程序接
受两个输入参数:显示数值( 0 ~ 9)和数字选择值( 0 ~ 3)。程序先打开 SPI 总线设备文件
和 GPIO 属性文件,然后设置 SPI 总线参数:总线极性、总线的最大频率、数据字的大小。
设置完成后,调用 show_led_num 函数执行数据发送操作,在该函数中把显示数值转化为位
选值、把数字选择值转化为段选值,再把位选值和段选值通过 SPI 总线发送到 U4 和 U6 的
74HC595 芯片的移位寄存器,最后通过 GPIO 产生一个上升沿信号使 74HC595 移位寄存器
的值输出到相关引脚,以控制数码管的点亮。
见源代码。
-
#include <stdint.h>
-
#include <unistd.h>
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <getopt.h>
-
#include <fcntl.h>
-
#include <sys/ioctl.h>
-
#include <linux/types.h>
-
#include <linux/spi/spidev.h>
-
-
#define SPI_DEVICE "/dev/spidev1.0"
-
#define GPIO_DEVICE "/sys/class/gpio/gpio117/value" /* gpio3.21 的属性文件 */
-
-
-
/* 显示数值和位选值的对照表 0 1 2 3 4 5 6 7 8 9 */
-
uint8_t led_value_table[] =
-
{
-
0xC0, 0xF9, 0xa4, 0xb0, 0x99,
-
0x92, 0x82, 0xF8, 0x80, 0x90
-
};
-
-
static uint8_t mode = 0;
-
static uint8_t bits = 8;
-
static uint32_t speed = 10000;
-
static uint16_t delay = 0;
-
-
static void show_led_num(int fd_spi, int fd_gpio, int value, int num)
-
{
-
int ret;
-
uint8_t tx[] = {
-
led_value_table[value], /* 把显示数值转化为位选值 */
-
(1 << num), /* 把数字选择值转化为段选值 */
-
};
-
-
struct spi_ioc_transfer tr_txrx[] =
-
{
-
{
-
.tx_buf = (unsigned long)tx,
-
.rx_buf = 0,
-
.len = 2,
-
.delay_usecs = delay,
-
.speed_hz = speed,
-
.bits_per_word = bits,
-
},
-
};
-
-
/* 把位选值和段选值通过 SPI 总线发送到 U4 和 U6 的移位寄存器 */
-
ret = ioctl(fd_spi, SPI_IOC_MESSAGE(1), &tr_txrx[0]);
-
if (ret == 1) {
-
printf("can't revieve spi message");
-
return;
-
}
-
-
/*
-
* 通过 GPIO 产生上升沿信号, 使 U4 和 U6 的移位寄存器的值输出到相关引脚,
-
* 以控制数码管的点亮
-
*/
-
write(fd_gpio, "0", 1);
-
usleep(100);
-
write(fd_gpio, "1", 1);
-
usleep(100);
-
}
-
-
-
int main(int argc, char *argv[])
-
{
-
int ret = 0;
-
int fd_spi = 0;
-
int fd_gpio = 0;
-
long led_value = 0;
-
int led_num = 0;
-
int i;
-
int disData[4];
-
-
if (argc != 2) { /* 输入参数必须为一个 */
-
printf("cmd : ./spi_led_test led_num \n ");
-
return -1;
-
}
-
-
led_value = atoi(argv[1]); /* 获取程序输入参数的数码管的显示值 */
-
if ((led_value) < 0 || (led_value > 10000)) { /* 该值必须在 0 ~ 9 之间 */
-
printf("led num just in 0 ~ 9999 \n");
-
return -1;
-
}
-
disData[0] = led_value/1000;
-
disData[1] = (led_value%1000)/100;
-
disData[2] = (led_value%100)/10;
-
disData[3] = led_value%10;
-
-
fd_spi = open(SPI_DEVICE, O_RDWR); /* 打开 SPI 总线的设备文件 */
-
if (fd_spi < 0) {
-
printf("can't open %s \n", SPI_DEVICE);
-
return -1;
-
}
-
-
fd_gpio = open(GPIO_DEVICE, O_RDWR); /* 打开 GPIO 设备的属性文件 */
-
if (fd_gpio < 0) {
-
printf("can't open %s device\n", GPIO_DEVICE);
-
return -1;
-
}
-
-
/*
-
* 这里 mode 的值为 0,这里SPI 总线的 SPI_CLK 在上升沿阶段,
-
* SPI_DIN 的信号有效,
-
* 这符合 74HC595 芯片把输入数据送入移位寄存器的要求。
-
*/
-
ret = ioctl(fd_spi, SPI_IOC_WR_MODE, &mode);
-
if (ret == -1) {
-
printf("can't set wr spi mode\n");
-
return -1;
-
}
-
-
ret = ioctl(fd_spi, SPI_IOC_WR_BITS_PER_WORD, &bits); /* 设置 SPI 的数据位 */
-
if (ret == -1) {
-
printf("can't set bits per word\n");
-
return -1;
-
}
-
-
ret = ioctl(fd_spi, SPI_IOC_WR_MAX_SPEED_HZ, &speed); /* 设置 SPI 的最大总线频率 */
-
if (ret == -1) {
-
printf("can't set max speed hz\n");
-
return -1;
-
}
-
-
while(1)
-
{
-
for(i = 0 ; i < 4; i++)
-
{
-
show_led_num(fd_spi, fd_gpio, disData[i], i); /* 实现数码管的控制 */
-
usleep(100);
-
}
-
}
-
-
//show_led_num(fd_spi, fd_gpio, led_value, led_num); /* 实现数码管的控制 */
-
close(fd_spi);
-
-
return ret;
-
}
3. 测试
3.1 可以单独控制某个数码管
上述代码可以通过交叉编译成 spi_led_test 程序文件,测试方法如下:
(1) 把 spi_led_test 文件上传到 EasyARM-i.MX283A 的任意目录;
(2) 在 AP-283Demo 板上的 J11A 和 J11C 的 CS、 CLK、 MISO、 MOSI 跳线用短路器短
接, 把 J7A 和 J7B 的 WCLK 跳线用短路器短接。
(3) 导出 GPIO3.21 的设备属性文件,并设置为出输出工作模式:
root@EasyARM-iMX283 /sys/class/gpio# echo 117 > export
root@EasyARM-iMX283 /sys/class/gpio# ls
export gpio68 gpiochip128 gpiochip64 unexport
gpio117 gpiochip0 gpiochip32 gpiochip96
root@EasyARM-iMX283 /sys/class/gpio# echo out > gpio117/direction
(4) 运行 spi_led_test 程序:
root@EasyARM-iMX28x /mnt# ./spi_led_test 9 2
该命令将控制数码管的第 3 个数字显示 9。
3.2 修改代码,让四个数码管同时显示,输入的命令为:
./spi_led_test number // number就是在数码管上显示的数字,比如如果为1234,
// 则在数码管上显示的就是 1234 。
3.3 秒表的实现
有空再修改吧....