armsom-w3单板机计算机之小白学习笔记(五)

前言:

本篇文章我们使用W3来学习如何在rockchip平台上配置spi接口和使用方法。

一.spi简介

1.什么是spi?

spi是Serial Peripheral Interface 翻译为串行外围设备接口。是一种同步通信的接口标准。

2.spi特点

  • spi是一种高速,全双工(并行通信:数据各位同时传送 串行通信:数据一位位顺序传送),同步串行总线。
  • spi有主从两种模式,通常由一个主设备和从设备或者多个从设备组从,spi不支持多主机。
  • spi通信至少需要四根线,分别是MISO,MOSI(),SCLK(收时钟信号),CS/SS(片选信号)。
  • 拓展:单工通信->单工通信只支持信号在一个方向上传输,也就只能发送和接收 ,半双工->半双工通信允许信号在两个方向上传输,但某一刻只允许发送或者接收。全双工通信->允许数据同时在两个方向上传输。

3.spi接口结构

  1. 主机:主机是SPI总线的控制者,它负责控制数据传输的方向和传输速度。
  2. 从机:从机是SPI总线的被控制者,它根据主机发出的指令,发出或接收数据。
  3. MOSI(Master Out Slave In):主机输出从机输入,用于传输从主机到从机的数据。
  4. MISO(Master In Slave Out):主机输入从机输出,用于传输从从机到主机的数据。
  5. CK(Serial Clock):时钟线,用于同步主机和从机之间的数据传输。
  6. CS(Chip Select):片选线,用于控制主机和从机之间的数据传输。

 4.spi通信

4.1通信原理

spi主设备和从设备都有一个串行移位寄存器,主设备通过向spi串行寄存器写入一个字节来发送一次数据传输(默认是先传输高位),每次数据传输主设备和从设备都会进行数据交换,也就是说在一个spi时钟周期内,收发是同时进行的。

4.2通信过程

主机先发出一个片选信号,然后在时钟信号的控制下,主机发出一个字节的数据,从机接收到数据之后,也会发出一个字节的数据,主机接收到数据之后,发出一个片选信号,结束一次通信。

spi总线在进行数据传输时,默认先传输高位,后传输低位,数据线为高电平表示1,数据线为低电平表示逻辑0,一个字节传输完成以后无需应答信号即可开启下一个字节的传输。

spi总线采用同步通信,在时钟线的第一个或者第二个跳变沿采集数据(主机测读数据),然后在紧接着的下一个跳变沿发送数据,8个时钟周期即可完成一个字节的传输。

 如果主设备要给从设备传输数据,主设备只需要忽略从设备接收到的数据即可,如果主设备要从从设备接收数据,主设备要要求从设备随机发送数据,从设备忽略掉从主设备接收到的数据即可。

5.spi硬件接口

RK3588旗舰芯片上可使用的spi接口有5组,W3开发板上40PIN引脚中有两组spi接口:

 

 二.spi子系统框架

spi的子系统框架和i2c是基本一样的,所以我们在这里不会讲得那么细节同样是分为三个部分

1.用户层

2.驱动层(spi设备驱动层,spi核心层,spi适配器驱动层)

3.硬件层

 1.用户层

用户层需要我们编写一些自己的测试代码,比如我们这里学习的是spi接口 那么我们需要编写一些测试的代码,下面的代码我使用的一个例子 ,大家可以参考一下。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>

#define SPI_DEV_PATH "/dev/spidev1.0"

int fd;
static unsigned mode = SPI_MODE_0;    // SPI工作模式,默认为模式0
static uint8_t bits = 8;               // 每个SPI传输字的位数,默认为8位
static uint32_t speed = 1000000;       // SPI传输速度,默认设置为1MHz
static uint16_t delay;

// SPI数据传输函数
void transfer(int fd, uint8_t const *tx, uint8_t *rx, size_t len)
{
    int ret;
    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,   // 发送数据缓冲区指针
        .rx_buf = (unsigned long)rx,   // 接收数据缓冲区指针
        .len = len,                    // 传输数据长度
        .delay_usecs = delay,          // 传输延迟,单位为微秒
        .speed_hz = speed,             // 传输速度,单位为赫兹
        .bits_per_word = bits,         // 每个传输字的位数
        .cs_change = 0,                // 是否在每次传输前切换片选信号,这里设为0表示不切换
    };

    ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr); // 发起SPI传输操作

    if (ret < 1) {
        perror("SPI transfer failed");  // 如果传输失败,打印错误信息
    }
}

// SPI初始化函数
void spi_init(void)
{
    int ret;
    // 打开 SPI 设备
    fd = open(SPI_DEV_PATH, O_RDWR);
    if (fd < 0) {
        perror("Can't open SPI device");  // 打开设备失败时打印错误信息并退出
        exit(1);
    }

    // 设置 SPI 工作模式
    ret = ioctl(fd, SPI_IOC_WR_MODE, &mode);
    if (ret == -1) {
        perror("Can't set SPI mode");  // 设置工作模式失败时打印错误信息并退出
        exit(1);
    }

    // 设置位数
    ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
    if (ret == -1) {
        perror("Can't set bits per word");  // 设置每个传输字位数失败时打印错误信息并退出
        exit(1);
    }

    // 设置SPI速度
    ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
    if (ret == -1) {
        perror("Can't set max speed");  // 设置SPI速度失败时打印错误信息并退出
        exit(1);
    }

    // 打印设置信息
    printf("SPI mode: 0x%x\n", mode);      // 打印设置的SPI工作模式
    printf("Bits per word: %d\n", bits);   // 打印每个传输字的位数
    printf("Max speed: %d Hz\n", speed);   // 打印设置的SPI传输速度
}

// 主函数
int main(int argc, char *argv[])
{
    if (argc != 2) {
        printf("Usage: %s <string_to_send>\n", argv[0]);  // 提示用户正确的使用方法
        return 1;
    }

    char *tx_buffer = argv[1]; // 获取要发送的字符串作为命令行参数

    // 初始化SPI接口
    spi_init();

    // 设置要接收数据的缓冲区
    unsigned char rx_buffer[strlen(tx_buffer) + 1];

    // 执行SPI数据传输
    transfer(fd, tx_buffer, rx_buffer, strlen(tx_buffer));

    // 打印发送和接收的数据
    printf("Sent: %s\n", tx_buffer);    // 打印发送的数据
    printf("Received: %s\n", rx_buffer); // 打印接收的数据

    // 关闭SPI设备
    close(fd);

    return 0;
}

2.驱动层

2.1spi设备驱动层

这一层分为设备树节点的编写和驱动程序的编写,spi设备树节点代码如下,在这里稍作解释:

  • 节点要使用的话,必须把status设置成okay。
  • spi_dev@0使用的是CS0,当然如果使用的是CS1的话,我们要把0改成1。
  • compatible这里的属性必须与驱动中的结构体(of_device_id中的成员compatible)一致。
  • reg这里的指的是总线的地址,与spi_dev@0保持一致,设置为0。

&spi1 {
    status = "okay";  // 表示SPI控制器启用正常工作

    // assigned-clock-rates = <200000000>; // 默认不用配置,SPI 设备工作时钟

    max-freq = <48000000>; /* spi internal clk, don't modify */
    // 设置SPI内部时钟频率上限为48 MHz,不建议修改

    // dma-names = "tx","rx"; // 使能DMA模式
    // 配置DMA通道名称,启用传输和接收DMA模式

    // rx-sample-delay-ns = <10>; // 默认不用配置,读采样延时
    // 配置读取数据时的采样延时,单位为纳秒

    spi_dev@0 {
        compatible = "rockchip,spidev";  // 指定设备兼容性字符串

        reg = <0>;  // SPI设备的注册地址

        spi-max-frequency = <12000000>;  // 设置SPI设备的最大传输频率为12 MHz

        spi-lsb-first;  // 设置IO传输顺序为LSB先传输
        // 若注释不允许使用GPIO

 另一个需要编写的是驱动代码,当然armsom-W3是开源的,所以我们不用编写,一些驱动的文件地址有(这些文件都是在我们的kernel里面包含的):

  • drivers/spi/spi.c spi驱动框架
  • drivers/spi/spi-rockchip.c rk spi各接口实现
  • drivers/spi/spidev.c 创建spi设备节点,用户态使用
  • drivers/spi/spi-rockchip-test.c spi测试驱动,需要自己手动添加到Makefile编译
  • Documentation/spi/spidev_test.c 用户态spi测试工具

 我们是使用测试程序来做这个spi的实验,所以我们需要选择用户态驱动drivers/spi/spidev.c,那怎么去用这个驱动呢?直接编译kernel就好了,编译完成之后会生成一个boot,这时候把boot烧录进开发板就可以啦。

当然有一些spi驱动的api接口(api接口就是kernel中用于操作spi设备的函数集合,提供了驱动程序与硬件设备进行通信的手段)我们也可以学习一下:

  • spi_register_driver()

    • 功能:注册SPI设备驱动程序。
    • 作用:将一个SPI设备的驱动程序注册到Linux内核中,使得系统能够识别和管理该设备。
  • spi_unregister_driver()

    • 功能:注销SPI设备驱动程序。
    • 作用:从Linux内核中移除之前注册的SPI设备驱动程序,当该设备不再需要时使用。
  • spi_setup()

    • 功能:设置SPI总线和设备的参数。
    • 作用:配置SPI总线的特性(如时钟频率、传输位数等)以及与之连接的具体SPI设备的参数。
  • spi_sync()

    • 功能:同步方式进行SPI数据传输。
    • 作用:以同步模式执行SPI数据的传输操作,等待数据传输完成后再返回。
  • spi_message_init()

    • 功能:初始化SPI消息结构。
    • 作用:准备一个SPI传输操作的消息结构,用于存储和描述后续传输的详细信息。
  • spi_message_add_tail()

    • 功能:向SPI消息添加传输操作。
    • 作用:将一个具体的SPI传输操作(如数据读取或写入)添加到之前初始化的SPI消息结构的末尾。
  • spi_transfer()

    • 功能:进行SPI数据传输。
    • 作用:执行实际的SPI数据传输操作,可能包括从SPI设备读取数据或向SPI设备发送数据。

下面是一个使用的例子

#include <linux/module.h>
#include <linux/spi/spi.h>

/* 
 * spi_example_probe - SPI设备探测函数
 * @spi: 指向spi_device结构的指针,表示探测到的SPI设备
 * 
 * 该函数在SPI设备探测到时调用,用于初始化SPI设备并进行数据传输测试。
 */
static int spi_example_probe(struct spi_device *spi)
{
    struct spi_message msg;  // 定义SPI消息结构
    struct spi_transfer xfer;  // 定义SPI传输结构
    char tx_buf[] = "Hello";  // 要发送的数据
    char rx_buf[10];  // 接收数据的缓冲区

    // 配置SPI设备
    spi_setup(spi);

    // 初始化SPI消息
    spi_message_init(&msg);

    // 初始化传输结构体,设置传输的相关参数
    memset(&xfer, 0, sizeof(xfer));
    xfer.tx_buf = tx_buf;  // 发送缓冲区
    xfer.rx_buf = rx_buf;  // 接收缓冲区
    xfer.len = sizeof(tx_buf);  // 传输长度

    // 将传输操作添加到消息中
    spi_message_add_tail(&xfer, &msg);

    // 同步方式发送SPI消息,阻塞直到传输完成
    spi_sync(spi, &msg);

    // 打印接收到的数据
    printk(KERN_INFO "Received: %s\n", rx_buf);

    return 0;  // 返回0表示成功
}

/* 
 * spi_example_remove - SPI设备移除函数
 * @spi: 指向spi_device结构的指针,表示要移除的SPI设备
 * 
 * 该函数在SPI设备移除时调用,用于清理设备。
 */
static int spi_example_remove(struct spi_device *spi)
{
    return 0;  // 返回0表示成功
}

/* 定义SPI驱动结构 */
static struct spi_driver spi_example_driver = {
    .driver = {
        .name = "spi_example",  // 驱动名称
        .owner = THIS_MODULE,  // 所属模块
    },
    .probe = spi_example_probe,  // 设备探测函数
    .remove = spi_example_remove,  // 设备移除函数
};

/* 
 * module_spi_driver - 宏定义,用于注册和注销SPI驱动
 * @spi_example_driver: 定义的SPI驱动结构
 * 
 * 该宏定义将会生成模块的初始化函数和退出函数,
 * 分别用于注册和注销SPI驱动。
 */
module_spi_driver(spi_example_driver);

MODULE_LICENSE("GPL");  // 指定模块的许可证

驱动程序作为内核的一部分进行编译,这样驱动程序在系统启动时就会被加载。配置内核时,在 make menuconfig 或 make nconfig 中选择相应的驱动程序选项,然后重新编译内核。

做完了这些就可以

2.2核心层

这一层是不需要我们去管的,但是我们需要知道有这样一个流程,就是核心层需要处理用户层传过来的数据 然后处理好,去调动适配器驱动层响应硬件。

2.3spi适配器驱动层

这一层主要是两个部分,一个是适配驱动代码(驱动代码会使spi控制器工作)一个是硬件收发函数,这两个代码块都不需要我们去编写,原厂的工程师已经把这些都写好了。

3硬件层

硬件层主要是spi控制器和具体的硬件,spi控制器不需要我们去管理 ,spi适配器驱动会帮我们处理和控制我们具体的硬件。

三.spi测试实验

1.硬件连接

我在实验中选择debug串口来连接主机,使用usb来烧录boot.然后把13脚和15脚短接,短接MOSI和MISO线路可以自发自收数据,同时在未段解释报告错误.

2.设备配置

使用 make menuconfig 命令打开配置 ,按照流程把控制器和驱动打开


 


 打开之后 保存配置即可.

3.编译&运行

确保开发板上已经安装了 gcc 编译器和相关的开发工具链:

apt update
apt upgrade
apt install gcc

 4.配置设备树&驱动

4.1添加节点

在合适的位置,添加设备树节点,比如在内核中arm/arch64boot/dts/rockchip/rockchip-rk3588-arm-som-w3.dts这个设备树中添加节点

&spi1 {
	status = "okay";
	//assigned-clock-rates = <200000000>; //默认不用配置,SPI 设备工作时钟
	max-freq = <48000000>; /* spi internal clk, don't modify */
	//dma-names = "tx","rx"; //使能DMA模式
	//rx-sample-delay-ns = <10>; //默认不用配置,读采样延时
	spi_dev@0 {
		compatible = "rockchip,spidev";
		reg = <0>;
		spi-max-frequency = <12000000>;
		spi-lsb-first; //IO 先传输 lsb		
	};
};

4.2加载驱动

  • 如果驱动是作为模块编译的,使用 insmod 命令加载模块:
insmd spi-rockchip.ko
  • 如果驱动是静态编译到内核的一部分,重新启动开发板以使其生效。

5.编写用户程序&编译内核

5.1编写spi测试程序

我们编写了一个spi_test.c的测试程序,这个程序里包含了一些调用spi驱动的函数

  1. 打开SPI设备文件:用户可以通过打开/dev/spidevX.Y文件来访问SPI设备,其中X是SPI控制器的编号,Y是SPI设备的编号。
  2. 配置SPI参数:用户可以使用ioctl命令SPI_IOC_WR_MODE、SPI_IOC_WR_BITS_PER_WORD和SPI_IOC_WR_MAX_SPEED_HZ来设置SPI模式、数据位数和时钟速度等参数。
  3. 发送和接收数据:用户可以使用read和write系统调用来发送和接收SPI数据。写入的数据将被传输到SPI设备,而从设备读取的数据将被存储在用户提供的缓冲区中。
  4. 关闭SPI设备文件:当不再需要与SPI设备通信时,用户应该关闭SPI设备文件。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>

#define SPI_DEV_PATH "/dev/spidev1.0"

int fd;
static unsigned mode = SPI_MODE_0;
static uint8_t bits = 8;
static uint32_t speed = 1000000; // 设置SPI速度为1MHz
static uint16_t delay;

void transfer(int fd, uint8_t const *tx, uint8_t *rx, size_t len)
{
    int ret;
    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = len,
        .delay_usecs = delay,
        .speed_hz = speed,
        .bits_per_word = bits,
        .cs_change = 0, // 设置为1以在每次传输前切换片选,这里不切换片选
    };

    ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);

    if (ret < 1) {
        perror("SPI transfer failed");
    }
}

void spi_init(void)
{
    int ret;
    // 打开 SPI 设备
    fd = open(SPI_DEV_PATH, O_RDWR);
    if (fd < 0) {
        perror("Can't open SPI device");
        exit(1);
    }

    // 设置 SPI 工作模式
    ret = ioctl(fd, SPI_IOC_WR_MODE, &mode);
    if (ret == -1) {
        perror("Can't set SPI mode");
        exit(1);
    }

    // 设置位数
    ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
    if (ret == -1) {
        perror("Can't set bits per word");
        exit(1);
    }

    // 设置SPI速度
    ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
    if (ret == -1) {
        perror("Can't set max speed");
        exit(1);
    }

    // 打印设置
    printf("SPI mode: 0x%x\n", mode);
    printf("Bits per word: %d\n", bits);
    printf("Max speed: %d Hz\n", speed);
}

int main(int argc, char *argv[])
{
    if (argc != 2) {
        printf("Usage: %s <string_to_send>\n", argv[0]);
        return 1;
    }

    char *tx_buffer = argv[1]; // 获取要发送的字符串作为命令行参数

    // 初始化SPI接口
    spi_init();

    // 设置要接收数据的缓冲区
    unsigned char rx_buffer[strlen(tx_buffer) + 1];

    // 执行SPI数据传输
    transfer(fd, tx_buffer, rx_buffer, strlen(tx_buffer));

    // 打印发送和接收的数据
    printf("Sent: %s\n", tx_buffer);
    printf("Received: %s\n", rx_buffer);

    // 关闭SPI设备
    close(fd);

    return 0;
}

5.2编译测试程序

我们把spi_test.c这个代码文件放到一个文件夹里 然后使用以下命令编译

gcc spi_test.c -o spi_test

5.3编译内核&烧录boot

在mobaxterm终端中在armsom-w3-bsp文件中使用./build.sh kernel编译内核,然后打开烧录工具,把boot烧录进去就可以了.

5.4检查spi设备

ls /dev/spi*

5.5在开发板上下载编译器

apt upgrade
apt update
apt install gcc

 5.6编译测试代码

gcc spi_test.c -o spi

6.运行测试

使用mobaxterm终端用串口连接到开发板,然后输入以下命令即可。

./spi_test "HelloSPI"

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

轻谈半窗月

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值