基于Linux使用spidev驱动OLED

嵌入式技术

1372人已加入

描述

如果不想编写spi设备驱动,那么linux内核提供了一个通用的spidev设备驱动,提供统一的字符设备操作,那么只需要在应用层读写和控制即可。以SPI OLED为例子,使用spidev驱动OLED,基于linux5.15.

参考源码:

tools/spi/spidev_fdx.c

tools/spi/spidev_test.c

1.配置使能spidev用户态驱动

- > Device Drivers                                                                                         │
         - > SPI support
      < * >   User mode SPI device driver support

设备驱动

驱动源文件:driver/spi/spidev.c

2.编写设备树

&ecspi2{
 fsl,spi-num-chipselects = < 1 >;
 cs-gpios = < &gpio1 29 GPIO_ACTIVE_LOW >;//GPIO1_29
 pinctrl-names = "default";
 pinctrl-0 = < &pinctrl_ecspi2 >;
 status = "okay";

 oled: ssd13306@0{
  compatible = "Justice,ssd13306";//匹配spidev驱动
  spi-cpol;
  spi-cpha;
  spi-rx-bus-width = < 0 >;
  spi-max-frequency = < 20000000 >;
  reset-gpios = < &gpio1 27 GPIO_ACTIVE_LOW >; 
  dc-gpios = < &gpio1 31 GPIO_ACTIVE_HIGH >;
  reg = < 0 >;
 };
};

注意这里的compatible 属性,在新版linux内核,可以写任意的字符串,最好不再写”spidev”,老版的是要写成”spidev”。给出的理由是: spidev should never be referenced in DT without a specific compatible string, it is a Linux implementation thing rather than a description of the hardware

3.修改spidev驱动,增加compatible

//driver/spi/spidev.c

static const struct of_device_id spidev_dt_ids[] = {
 { .compatible = "rohm,dh2228fv" },
 { .compatible = "lineartechnology,ltc2488" },
 { .compatible = "semtech,sx1301" },
 { .compatible = "lwn,bk4" },
 { .compatible = "dh,dhcom-board" },
 { .compatible = "menlo,m53cpld" },
 { .compatible = "cisco,spi-petra" },
 { .compatible = "micron,spi-authenta" },
 { .compatible = "Justice,ssd13306" },
 {},
};

在最后一行增加自己的匹配compatible。

4.用户态读写、控制spi设备

当设备树和spidev成功匹配后,就为我们的spi设备生成了一个设备节点/dev/spidevx.y。

x表示spi控制器的软件枚举的总线号,y表示这个spi控制器的片选号。

设备树aliases会影响spi控制器的软件枚举的总线号,如我使用ecspi2,芯片上spi控制器的第2个spi控制器,但是我的设备树上面写了aliases,因此我呈现的就是/dev/spidev1.0

aliases {
  ...
  spi0 = &ecspi1;
  spi1 = &ecspi2;
  spi2 = &ecspi3;
  spi3 = &ecspi4;
}

设备驱动

/sys/class/spidev下可以确认spidev枚举出了多少个spi设备

root@imx6ull /sys/class/spidev# ls
spidev1.0

设置传输模式

spi 核心会根据spi device的mode标志,来决定一些传输的模式,比如时钟极性、LSB等等

这个标志是32位,低16位是用户空间设置,高16位是内核控制,因此不能有冲突。如果在设置之前读取,读取到的模式和设备树定义的一样。

以下是用户空间的宏定义。

include/uapi/linux/spi/spi.h

#define SPI_CPHA  _BITUL(0) /* clock phase */
#define SPI_CPOL  _BITUL(1) /* clock polarity */

#define SPI_MODE_0  (0|0)  /* (original MicroWire) */
#define SPI_MODE_1  (0|SPI_CPHA)
#define SPI_MODE_2  (SPI_CPOL|0)
#define SPI_MODE_3  (SPI_CPOL|SPI_CPHA)
#define SPI_MODE_X_MASK  (SPI_CPOL|SPI_CPHA)

#define SPI_CS_HIGH  _BITUL(2) /* chipselect active high? */
#define SPI_LSB_FIRST  _BITUL(3) /* per-word bits-on-wire */
#define SPI_3WIRE  _BITUL(4) /* SI/SO signals shared */
#define SPI_LOOP  _BITUL(5) /* loopback mode */
#define SPI_NO_CS  _BITUL(6) /* 1 dev/bus, no chipselect */
#define SPI_READY  _BITUL(7) /* slave pulls low to pause */
#define SPI_TX_DUAL  _BITUL(8) /* transmit with 2 wires */
#define SPI_TX_QUAD  _BITUL(9) /* transmit with 4 wires */
#define SPI_RX_DUAL  _BITUL(10) /* receive with 2 wires */
#define SPI_RX_QUAD  _BITUL(11) /* receive with 4 wires */
#define SPI_CS_WORD  _BITUL(12) /* toggle cs after each word */
#define SPI_TX_OCTAL  _BITUL(13) /* transmit with 8 wires */
#define SPI_RX_OCTAL  _BITUL(14) /* receive with 8 wires */
#define SPI_3WIRE_HIZ  _BITUL(15) /* high impedance turnaround */

/*
 * All the bits defined above should be covered by SPI_MODE_USER_MASK.
 * The SPI_MODE_USER_MASK has the SPI_MODE_KERNEL_MASK counterpart in
 * 'include/linux/spi/spi.h'. The bits defined here are from bit 0 upwards
 * while in SPI_MODE_KERNEL_MASK they are from the other end downwards.
 * These bits must not overlap. A static assert check should make sure of that.
 * If adding extra bits, make sure to increase the bit index below as well.
 */
#define SPI_MODE_USER_MASK (_BITUL(16) - 1)

读取和设置模式

如上面所说,mode是一个32位的标志,你要精准设置这个标志,每一个位的定义都在上面列举。

//使用到的宏
SPI_IOC_RD_MODE
SPI_IOC_RD_MODE32

SPI_IOC_WR_MODE
SPI_IOC_WR_MODE32

//伪代码
/*读取传输模式*/
u32 mode32;
u8 mode;
fd = open("/dev/spidevx.y",O_RDWR);
if (ioctl(fd, SPI_IOC_RD_MODE32, &mode32) < 0) {
            perror("SPI rd_mode");
            return;
 }

if (ioctl(fd, SPI_IOC_RD_MODE, &mode) < 0) {
            perror("SPI rd_mode");
            return;
 }
/*设置传输模式*/
u32 mode32;
u8 mode;
fd = open("/dev/spidevx.y",O_RDWR);
if (ioctl(fd, SPI_IOC_WR_MODE32, &mode32) < 0) {
            perror("SPI rd_mode");
            return;
 }

if (ioctl(fd, SPI_IOC_WR_MODE, &mode) < 0) {
            perror("SPI rd_mode");
            return;
 }

读取和设置LSB传输

这个也是读取和设置spi device的mode的某位,只不过单独定义了一个命令

//使用到的宏
SPI_IOC_RD_LSB_FIRST
SPI_IOC_WR_LSB_FIRST

//伪代码
/*读取LSB模式*/
u8 lsb;
fd = open("/dev/spidevx.y",O_RDWR);
if (ioctl(fd, SPI_IOC_RD_LSB_FIRST, &lsb) < 0) {
            perror("SPI rd_lsb_fist");
            return;
 }
/*设置LSB模式*/
if (ioctl(fd, SPI_IOC_WR_LSB_FIRST, &lsb) < 0) {
            perror("SPI rd_lsb_fist");
            return;
 }

读取和设置字长

字长是每次 SPI 传输中发送或接收的数据位数,由 SPI 主设备或从设备指定。一般情况下,字长的值为 8, 16, 24 或 32。

这个命令对于应用程序读取和设置 SPI 总线的字长非常有用。通过此命令,应用程序可以动态地获取 SPI 总线的字长,以根据不同的需求来发送和接收可变长度的数据。

//使用到的宏
SPI_IOC_RD_BITS_PER_WORD
SPI_IOC_WR_BITS_PER_WORD

//伪代码
/*读取字长*/
u8 bits;
fd = open("/dev/spidevx.y",O_RDWR);
if (ioctl(fd, SPI_IOC_RD_LSB_FIRST, &bits) < 0) {
            perror("SPI rd_lsb_fist");
            return;
 }
/*设置字长*/
if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) {
            perror("SPI rd_lsb_fist");
            return;
 }

读取和设置SPI传输速度

默认的SPI传输速度是在设备树定义的,应用程序从此可以设置SPI速率了

//使用到的宏
SPI_IOC_RD_MAX_SPEED_HZ  
SPI_IOC_WR_MAX_SPEED_HZ

//伪代码
/*读取字长*/
u8 bits;
fd = open("/dev/spidevx.y",O_RDWR);
if (ioctl(fd, SPI_IOC_RD_LSB_FIRST, &bits) < 0) {
            perror("SPI rd_lsb_fist");
            return;
 }
/*设置字长*/

if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) {
            perror("SPI rd_lsb_fist");
            return;
 }

发送和接收数据

在这里,spidev驱动的write和read方法是办双工的,意味着同时只能发或接收。

但是ioctl方法是可以全双工的,如果我们的设备需要全双工,可以使用ioctl.

应用程序使用struct spi_ioc_transfer 表示一个传输,和驱动的spi_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;
 __u8  tx_nbits;
 __u8  rx_nbits;
 __u8  word_delay_usecs;
 __u8  pad;
};
  1. 全双工通信

使用SPI_IOC_MESSAGE命令

大多数SPI传输都使用8位长,因此下面的例子使用的tx_buf,rx_buf缓冲区数据是8位的.

uint8_t tx[25] = {0x55};
uint8_t rx[ARRAY_SIZE(tx)] = {0, };
struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = ARRAY_SIZE(tx),
        .delay_usecs = delay,
        .speed_hz = 20000000,
        .bits_per_word = 8,
    };

   ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
 for (ret = 0; ret < ARRAY_SIZE(tx); ret++) {
        if (!(ret % 6))
            puts("");
        printf("%.2X ", rx[ret]);
    }
    puts("");

定义发送和接收缓冲区,构造spi_ioc_transfer,使用ioctl完成全双工命令。

SPI_IOC_MESSAGE(1)表示发送多少个spi_ioc_transfer,上述例子发送了一个spi_ioc_transfer,对应一个spi_transfer。

spidev_fdx.c中发送了两个

2.半双工通信

使用到write和read系统读写spidev的都是半双工通信的,直接调用即可。spidev自动构造spi core能够使用的spi_massage。

一次write对应一个spi_massage,传入的数据量就是spi_transfer的长度。

一些开源工具

在buildroot中可以找到关于SPI的一些开源工具,如spi-config、spi-pipe。

还有两个在内核源码中可以找到,进入make就可以编译得到

tools/spi/spidev_fdx.c

tools/spi/spidev_test.c

总之这些工具都基于spidev通用设备驱动以及对应的ioctl命令实现。因此在使用这些工具的时候,最好用的是spidev而不是自己写的驱动。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分