在ARM Linux应用层下使用SPI驱动WS2812

文章目录

1、前言

事情是这样的,前段时间,写了一个基于RK356x/RK3588的WS2812驱动,实验发现,单独点亮RGB灯倒是没什么问题。但点灯只是第一步,因为后面还要做呼吸灯,所以又经过实验发现,在基于此驱动实现的呼吸灯应用效果差强人意,过程中会频繁出现灯灭和颜色误识别的情况。为什么呢?在之前的驱动里,是利用了程序执行的延时来完成码0和码1的传输,加上呼吸灯需要对WS2812频繁操作,所以容易出现不稳定的情况。

失败的效果如下(四个颜色呼吸渐变):

2、结果展示

再看SPI方式驱动的WS2812呼吸灯效果(四个颜色呼吸渐变):

3、接线

SPI控制器的DO接到WS2812的IN

4、SPI驱动WS2812原理

需要确定三个参数:0码要发送的字节、1码要发送的字节、SPI时钟频率

4.1、0码要发送的字节

0码的高电平(T0H)时间需要控制在220ns ~ 380ns,低电平(T0L)时间需要控制在580ns ~ 1.6us。

这里T0H取300ns,T0L取700ns,加起来刚好凑整1us的周期,T0H占30%,T0L占70%。

现在利用各自的占比来确认要发送的8个bit:

T0H所占的bit:(30% / 100%) * 8 = 2.4bit≈2bit

T0L所占的bit:(70% / 100%) * 8 = 5.6bit≈6bit

所以0码使用11000000(0xC0)表示。最后SPI发送0xC0就会先拉高300ns,拉低700ns(这是设想,要确定SPI时钟频率后才能真正实现)。

4.2、1码要发送的字节

1码的高电平(T1H)时间需要控制在580ns ~ 1.6us,低电平(T1L)时间需要控制在220ns ~ 420ns。

这里T1H取700ns,T1L取300ns,加起来刚好凑整1us的周期,T1H占70%,T1L占30%。

现在利用各自的占比来确认要发送的8个bit:

T1H所占的bit:(70% / 100%) * 8 = 5.6bit≈6bit

T1L所占的bit:(30% / 100%) * 8 = 2.4bit≈2bit

所以1码使用11111100(0xFC)表示。最后SPI发送0xFC就会先拉高700ns,拉低300ns(这是设想,要确定SPI时钟频率后才能真正实现)。

4.3、SPI时钟频率

频率的确认是最关键的,这决定了是否能正确发送0码和1码。

上面讲到发送一个字节需要1us的周期,所以SPI传输每bit的时间为:1000/8=125ns,换成频率即是1/125=8Mhz,所以SPI时钟频率需要设置为8Mhz。

5、点亮RGB

SPI发送函数的实现如下:

c 复制代码
/* spi.c */

... 

/*****************************
* @brief : 向 SPI 总线写入n个字节数据
* @param : send_buf - 待写入的数据
* @param : send_buf_len - 待写入的数据长度
* @return: 无返回值
* @note  : 通过 SPI 总线发送n个字节的数据。
*****************************/
void spi_write_nbyte_data(unsigned char *send_buf, unsigned int send_buf_len)
{
    struct spi_ioc_transfer	xfer[2];
    unsigned char recv_buf[send_buf_len];
	int status;

    if(send_buf == NULL || send_buf_len < 1)
        return;

    memset(xfer, 0, sizeof(xfer));
    memset(recv_buf, 0, sizeof(send_buf_len));

	xfer[0].tx_buf = (unsigned long)send_buf;
    xfer[0].rx_buf = (unsigned long)recv_buf;
	xfer[0].len = send_buf_len;

	status = ioctl(fd_spidev, SPI_IOC_MESSAGE(1), xfer);
	if (status < 0) {
		perror("SPI_IOC_MESSAGE");
		return;
	}
}

...

5.1、亮绿灯

我这个灯是按照GRB的顺序发送数据。

c 复制代码
/* main.c */

...

unsigned char send_buf[24] = {0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc};
spi_write_nbyte_data(send_buf, sizeof(send_buf));

...

5.2、亮红灯

c 复制代码
/* main.c */

...

unsigned char send_buf[24] = {0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc};
spi_write_nbyte_data(send_buf, sizeof(send_buf));

...

5.3、亮蓝灯

c 复制代码
/* main.c */

...

unsigned char send_buf[24] = {0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc};
spi_write_nbyte_data(send_buf, sizeof(send_buf));

...

5.4、完整程序

spi.h

c 复制代码
/*
*
*   file: spi.h
*   updata: 2024-12-05
*
*/

#ifndef _SPI_H
#define _SPI_H

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

typedef struct spi_operations
{
    void (*spi_write_then_read)(unsigned char *send_buf, unsigned int send_buf_len, unsigned char *recv_buf, unsigned int recv_buf_len);
    void (*spi_write_byte_data)(unsigned char data);
	void (*spi_write_nbyte_data)(unsigned char *send_buf, unsigned int send_buf_len);
	pthread_mutex_t *mutex;
}spi_operations_t;

typedef enum
{
	SPIMODE0 = SPI_MODE_0,
	SPIMODE1 = SPI_MODE_1,
	SPIMODE2 = SPI_MODE_2,
	SPIMODE3 = SPI_MODE_3,
}SPI_MODE;
 
typedef enum
{
    S_1M    = 1000000,
	S_6_75M = 6750000,
	S_8M    = 8000000,
	S_13_5M = 13500000,
	S_27M   = 27000000,
}SPI_SPEED;

int spi_init(const char *spi_dev);
void spi_exit();
spi_operations_t *get_spi_ops();

#endif

spi.c

c 复制代码
/*
*
*   file: spi.c
*   updata: 2024-12-05
*
*/

#include "spi.h"

static int fd_spidev;
static int init_flag = 0;       // 1已初始化 0未初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

/*****************************
 * @brief : 初始化 SPI 设备
 * @param : spi_dev - SPI 设备路径
 * @return: 成功返回 0,失败返回 -1
 * @note  : 初始化 SPI 接口,配置 SPI 参数。
 *****************************/
int spi_init(const char *spi_dev)
{
    int ret; 
    SPI_MODE mode;
    char spi_bits;
    SPI_SPEED spi_speed;

    fd_spidev = open(spi_dev, O_RDWR);
	if (fd_spidev < 0) {
		printf("open %s err\n", spi_dev);
		return -1;
	}

    /* mode */
    mode = SPIMODE0;
    ret = ioctl(fd_spidev, SPI_IOC_WR_MODE, &mode);                //mode 0
    if (ret < 0) {
		printf("SPI_IOC_WR_MODE err\n");
		return -1;
	}

    /* bits per word */
    spi_bits = 8;
    ret = ioctl(fd_spidev, SPI_IOC_WR_BITS_PER_WORD, &spi_bits);   //8bits 
    if (ret < 0) {
		printf("SPI_IOC_WR_BITS_PER_WORD err\n");
		return -1;
	}

    /* speed */
    spi_speed = (uint32_t)S_8M;
    ret = ioctl(fd_spidev, SPI_IOC_WR_MAX_SPEED_HZ, &spi_speed);    //1MHz    
    if (ret < 0) {
		printf("SPI_IOC_WR_MAX_SPEED_HZ err\n");
		return -1;
	}

    init_flag = 1;
    return 0;
}

/*****************************
 * @brief : 向 SPI 总线写入数据并读取数据
 * @param : send_buf     - 发送数据的缓冲区
 *          send_buf_len - 发送数据的长度
 *          recv_buf     - 接收数据的缓冲区
 *          recv_buf_len - 接收数据的长度
 * @return: 无返回值
 * @note  : 通过 SPI 总线发送和接收数据,发送的数据通过 `send_buf`,接收到的数据存放在 `recv_buf` 中。
 *****************************/
static void spi_write_then_read(unsigned char *send_buf, unsigned int send_buf_len, unsigned char *recv_buf, unsigned int recv_buf_len)
{
    struct spi_ioc_transfer	xfer[2];
	int status;

    if(init_flag == 0)
    {
        perror("spidev can not init!\n");
        return;
    }

    if(send_buf == NULL || recv_buf == NULL)
        return;

    if(send_buf_len < 1 || recv_buf_len < 1)
        return;

    memset(xfer, 0, sizeof(xfer));

	xfer[0].tx_buf = (unsigned long)send_buf;
	xfer[0].len = send_buf_len;

	xfer[1].rx_buf = (unsigned long)recv_buf;
	xfer[1].len = recv_buf_len;

	status = ioctl(fd_spidev, SPI_IOC_MESSAGE(2), xfer);
	if (status < 0) {
		perror("SPI_IOC_MESSAGE");
		return;
	}
}

/*****************************
 * @brief : 向 SPI 总线写入一个字节数据
 * @param : data - 待写入的数据字节
 * @return: 无返回值
 * @note  : 通过 SPI 总线发送一个字节的数据。
 *****************************/
static void spi_write_byte_data(unsigned char data)
{
    unsigned char buff[1] = {data};

    if(init_flag == 0)
    {
        perror("spidev can not init!\n");
        return;
    }

    write(fd_spidev, &buff, 1);
}

/*****************************
 * @brief : 向 SPI 总线写入n个字节数据
 * @param : send_buf - 待写入的数据
 * @param : send_buf_len - 待写入的数据长度
 * @return: 无返回值
 * @note  : 通过 SPI 总线发送n个字节的数据。
 *****************************/
static void spi_write_nbyte_data(unsigned char *send_buf, unsigned int send_buf_len)
{
    struct spi_ioc_transfer	xfer[2];
    unsigned char recv_buf[send_buf_len];
	int status;

    if(init_flag == 0)
    {
        perror("spidev can not init!\n");
        return;
    }

    if(send_buf == NULL || send_buf_len < 1)
        return;

    memset(xfer, 0, sizeof(xfer));
    memset(recv_buf, 0, sizeof(send_buf_len));

	xfer[0].tx_buf = (unsigned long)send_buf;
    xfer[0].rx_buf = (unsigned long)recv_buf;
	xfer[0].len = send_buf_len;

	status = ioctl(fd_spidev, SPI_IOC_MESSAGE(1), xfer);
	if (status < 0) {
		perror("SPI_IOC_MESSAGE");
		return;
	}
}

/*****************************
 * @brief : 关闭 SPI 
 * @param : none
 * @return: 无返回值
 *****************************/
void spi_exit()
{
    if(fd_spidev >= 0)
        close(fd_spidev);
    
    init_flag = 0;
}

static spi_operations_t spi_ops = {
    .spi_write_then_read = spi_write_then_read,
    .spi_write_byte_data = spi_write_byte_data,
    .spi_write_nbyte_data = spi_write_nbyte_data,
    .mutex = &mutex,
};

/*****************************
 * @brief : 获取SPI操作函数
 * @param : none
 * @return: 返回spi_operations_t结构体指针
 *****************************/
spi_operations_t *get_spi_ops()
{
    return &spi_ops;
};

main.c

c 复制代码
/*
*
*   file: main.c
*   update: 2024-12-05
*   usage: 
*       sudo gcc -o main main.c
*       sudo ./main FF0000
*
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <math.h>
#include <signal.h> 
#include <pthread.h>

#include "spi.h"

unsigned char send_buf[24];
void update_sendbuff(unsigned char r, unsigned char g, unsigned char b)
{
    int i = 0;

    // update g
    for (i = 0; i < 8; i++) 
    {
        send_buf[i] = (g & 0x80) ? (0xFC) : (0xC0);		
        g <<= 1;		
    }

    // update r
    for (i = 8; i < 16; i++) 
    {
        send_buf[i] = (r & 0x80) ? (0xFC) : (0xC0);	
        r <<= 1;			
    }

    // update b
    for (i = 16; i < 24; i++) 
    {
        send_buf[i] = (b & 0x80) ? (0xFC) : (0xC0);	
        b <<= 1;			
    }
}

int main(int argc, char **argv) 
{
    int ret;
    unsigned char r, g, b;
    spi_operations_t *spi_ops;      // spi操作函数

    // 初始化spi
    ret = spi_init("/dev/spidev3.0");
    if(ret < 0)
        return -1;
    spi_ops = get_spi_ops();
    
    // 参数数量检查
    if(argc != 2) 
    {
        printf("Usage: %s <hex_color>\n", argv[0]);
        printf("e.g. : %s FF0000\n", argv[0]);
        return -1;
    }

    /* 参数1检查 */
    if(strlen(argv[1]) != 6)
    {
        printf("Error: The first argument has illegal length.\n");
        printf("e.g. : %s FF0000\n", argv[0]);
        return -1;
    }
    if (sscanf(argv[1], "%2hhx%2hhx%2hhx", &r, &g, &b) != 3) 
    {  
        printf("Error: Invalid hex color format.\n");  
        return -1;  
    }
    
    // 更新颜色数据
    update_sendbuff(r, g, b);

    spi_ops->spi_write_nbyte_data(send_buf, sizeof(send_buf));     

    return 0;
}

6、RGB呼吸灯

RGB三色灯可以通过控制红、绿、蓝三个颜色的分量实现全真色彩显示,同时可实现256级亮度显示。

如熄灭灯是0x000000,点亮蓝灯是0x0000ff,所以可以通过控制低16位来调节蓝灯的亮度,值越小亮度越小。

如发送0x000011时,蓝灯的亮度如下:

发送0x000055时,蓝灯的亮度如下:

发送0x0000ff时,蓝灯的亮度如下:

红灯则是调节中间16位、绿灯是调节高16位。

7、总结

参考文章:SPI驱动ws2812详细解说

相关推荐
虾..6 小时前
Linux 软硬链接和动静态库
linux·运维·服务器
Evan芙7 小时前
Linux常见的日志服务管理的常见日志服务
linux·运维·服务器
hkhkhkhkh1238 小时前
Linux设备节点基础知识
linux·服务器·驱动开发
HZero.chen10 小时前
Linux字符串处理
linux·string
张童瑶10 小时前
Linux SSH隧道代理转发及多层转发
linux·运维·ssh
汪汪队立大功12310 小时前
什么是SELinux
linux
石小千10 小时前
Linux安装OpenProject
linux·运维
柏木乃一10 小时前
进程(2)进程概念与基本操作
linux·服务器·开发语言·性能优化·shell·进程
Lime-309010 小时前
制作Ubuntu 24.04-GPU服务器测试系统盘
linux·运维·ubuntu
百年渔翁_肯肯10 小时前
Linux 与 Unix 的核心区别(清晰对比版)
linux·运维·unix