iMX6ULL驱动开发 | OLED显示屏SPI驱动实现(SH1106,ssd1306)

news2024/9/23 23:35:18

周日业余时间太无聊,又不喜欢玩游戏,大家的兴趣爱好都是啥?我觉得敲代码也是一种兴趣爱好。正巧手边有一块儿0.96寸的OLED显示屏,一直在吃灰,何不把玩一把?于是说干就干,最后在我的imax6ul的linux开发板上使用spi用户态驱动成功点亮。这里总结下过程,分享给有需要的小伙伴。

前言

本文主要介绍在imax6ul-mini开发板上如何驱动OLED显示屏外设,总结下过程。由于板子默认是spi接口的,这里先玩一把spi接口的驱动,后续计划改为i2c的接口驱动再玩一次。

我的环境资源:

Linux内核:linux-4.1.15

所用开发板:正点原子imax6ul-mini

所用OLED 屏幕:中景园电子0.96 寸OLED 显示屏12864液晶屏模块(支持spi和i2c接口)

所用OLED 驱动芯片:SH1106

完整源码下载地址:

https://download.csdn.net/download/qq8864/88117562

效果截图:

实现方案

想要驱动中景园电子的这款OLED显示屏,方案有很多。这个模块同时支持spi和i2c接口,所以肯定需要使用linux的spi或i2c驱动。

以 spi驱动为例,在嵌入式Linux下,实现SPI驱动的方式有多种。以下是其中几种常见的方式:

1. 使用GPIO控制模拟SPI:使用GPIO接口控制SPI总线的时序和数据传输,需要自行编写驱动程序来实现SPI通信。

2. 使用SPI框架驱动:Linux内核提供了SPI框架驱动,可以通过注册SPI设备和驱动来实现SPI通信。需要编写SPI设备和驱动的代码。

3. 使用spidev驱动:spidev是Linux内核提供的一个通用的SPI设备驱动接口,可以简化SPI设备的使用。它提供了用户空间的API,通过打开/dev/spidev设备文件,使用ioctl函数进行SPI通信。使用spidev驱动可以方便地在用户空间进行SPI通信,而无需编写内核驱动程序。

这里要介绍的实现方式是使用spidev驱动驱动。原因是因为操作OLED屏幕,需要自定义一系列的操作接口如oled_dispString() ,oled_clear()等。虽然使用方式2是较为流行的一种,但是需要注册到字符设备框架下,提供文件系统的操作接口的方式用,用起来稍显麻烦,还需再次封装一下。

spidev驱动是一个通用的SPI设备驱动接口,它允许用户空间通过简单的API与SPI设备进行通信。用户可以通过打开/dev/spidev设备文件,使用ioctl函数进行SPI通信。spidev驱动提供了一些常用的操作函数,如配置SPI模式、设置时钟频率、传输数据等。使用spidev驱动可以方便地在用户空间进行SPI通信,而无需编写内核驱动程序。

与传统的SPI驱动相比,spidev驱动的优点是简单易用,无需编写内核驱动程序,只需在用户空间使用ioctl函数进行SPI通信。但是,spidev驱动也有一些限制,例如无法支持中断和DMA传输等高级功能。如果需要使用这些高级功能,可能需要编写自定义的SPI驱动程序。

实现过程

首先需要确认内核配置开启了spidev驱动。参见博文:嵌入式linux通用spi驱动之spidev使用总结_特立独行的猫a的博客-CSDN博客

修改设备树

修改imx6ull-14x14-evk.dts文件,该设备树文件位于内核源码 linux/arch/arm/boot/dts/目录下。

&ecspi3 {
        fsl,spi-num-chipselects = <2>;/*cs管脚数配置*/
        cs-gpios = <0>,<&gpio1 20 GPIO_ACTIVE_LOW>;/*cs管脚配置*/
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_ecspi3>;
        status = "okay";/* status属性值为"okay" 表示该节点使能*/
 
	spidev: icm20608@0 {
	compatible = "alientek,icm20608";
        spi-max-frequency = <8000000>;
        reg = <0>;/*spi设备是没有设备地址的, 这里是指使用spi控制器的cs-gpios里的第几个片选io */
    };
 
	oled: oledsh1106@1 {
	compatible = "yang,oledsh1106";/*重要,会匹配spidev.c中指定的compatible*/
	spi-cpol;/*配置spi信号模式*/
	spi-cpha;
	spi-max-frequency = < 8000000 >;/* 指定spi设备的最大工作时钟 */
    reg = <1>;
    };
};

以上需要注意的是:如果该spi接口下挂载有多个从设备,需要设置fsl,spi-num-chipselects = <2>;默认该值为1。还有需要注意的地方是,cs-gpios 片选信号需要配置对应的个数。以上的为配置了两路片选GPIO管脚,第一个默认的,第二个是指定的。如果仅有一个从设备,可以配置cs-gpio就行了。注意cs-gpio和cs-gpios的区别,带s的标识可以有多个。

修改spidev驱动

默认的spidev.c驱动文件中,是没有匹配你添加的设备的。因此需要修改spidev.c源码,增加compatible匹配。spidev.c源码文件位于linux/drivers/spi/spidev.c

/* The main reason to have this class is to make mdev/udev create the
 * /dev/spidevB.C character device nodes exposing our userspace API.
 * It also simplifies memory management.
 */
 
static struct class *spidev_class;
 
//#ifdef CONFIG_OF
static const struct of_device_id spidev_dt_ids[] = {
	{ .compatible = "rohm,dh2228fv" },
  { .compatible = "yang,oledsh1106" },
	{},
};
MODULE_DEVICE_TABLE(of, spidev_dt_ids);
//#endif

编译内核和设备树

#加载环境
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
#编译内核
make zImage -j16
#编译指定的设备树
make imx6ull-14x14-nand-4.3-480x272-c.dtb

为了方便调试,更新内核和设备树文件,建议最好使用sd卡启动,这样把sd卡抠出来插入电路上,可以很方便的更新内核和设备树文件。 

设备树查看

内核和设备树更新后,启动开发板。可以看下spidev驱动是否生效了。

查看设备树是否有新添加的节点:

更新设备树到板子上后,能够查看到如下生成spi设备节点(spidev2.1):

经过以上过程,已经成功了一大半啦。或者可以用工具测试下spi驱动接口。

OELD驱动实现

在以上spidev总线驱动就绪的基础上,OELD驱动实现就简单啦。

先看下oled模块板子的接线:

DO对应SPI接口的CLK, D1(spi)数据线对应SPI接口了MOSI。

注意:由于这个屏幕显示不存在读取的情况,SPI的MISO口并未使用,且莫把线接错了。(比如我一开始就把MISO接到DC上啦,这是错的。)。DC口是啥?这是该模块的数据和命令选择管脚,分为发送指令和发送数据两周类型的操作。当发送指令时,DC口需要输出高电平,当发送数据时,DC口需要发送低电平。

SPI(Serial Peripheral Interface)是一种串行外设接口协议,用于在微控制器或其他设备之间进行通信。SPI使用四根线进行通信:SCLK(时钟线)、MOSI(主设备输出从设备输入线)、MISO(主设备输入从设备输出线)和SS(片选线)。

MOSI(Master Out Slave In)是主设备向从设备发送数据的线路,而MISO(Master In Slave Out)是从设备向主设备发送数据的线路。这两条线的功能是相反的,主设备通过MOSI将数据发送给从设备,从设备通过MISO将数据发送给主设备。

SPI协议中的数据传输是双向的,主设备和从设备可以同时发送和接收数据。因此,如果只需要进行写操作,理论上可以只使用MOSI线,而将MISO线悬空不连接。但在实际应用中,为了保持SPI接口的完整性和稳定性,通常还是会将MISO线连接起来,即使在写操作时不使用。

连接MISO线的好处是可以实现双向通信的灵活性,以备将来可能需要读取从设备的数据。另外,MISO线上的数据也可以用于进行错误检测和校验,以提高数据传输的可靠性。

驱动实现

spidev驱动操作

使用spidev驱动的操作方法,大致过程操作如下:

1. 打开SPI设备:

int spi_fd;
spi_fd = open("/dev/spidev0.0", O_RDWR);
if (spi_fd < 0) {
    perror("Failed to open SPI device");
    return -1;
}

2. 设置SPI模式、速度和位数:

int mode = SPI_MODE_0;
int speed = 1000000;
int bits_per_word = 8;
if (ioctl(spi_fd, SPI_IOC_WR_MODE, &mode) < 0) {
    perror("Failed to set SPI mode");
    return -1;
}
if (ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0) {
    perror("Failed to set SPI speed");
    return -1;
}
if (ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &bits_per_word) < 0) {
    perror("Failed to set SPI bits per word");
    return -1;
}

3. 创建spi_ioc_message结构体,并设置相关字段:

struct spi_ioc_transfer transfer;
memset(&transfer, 0, sizeof(struct spi_ioc_transfer));
transfer.tx_buf = (unsigned long)tx_buffer;  // 发送缓冲区
transfer.rx_buf = (unsigned long)rx_buffer;  // 接收缓冲区
transfer.len = length;  // 数据长度
transfer.speed_hz = speed;  // 传输速度
transfer.bits_per_word = bits_per_word;  // 每个字的位数
transfer.cs_change = 1;  // 控制片选信号的行为

其中,transfer.cs_change为1表示每次传输前会拉低片选信号,传输完成后会拉高片选信号。

4. 发送命令和数据:

if (ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer) < 0) {
    perror("Failed to send SPI message");
    return -1;
}

其中,SPI_IOC_MESSAGE(1)表示发送1个spi_ioc_transfer结构体。

OELD驱动接口封装

#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include "oledfont.h"
#include "bmp.h"
#include "oled.h"


#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))


static void pabort(const char *s)
{
	perror(s);
	abort();
}

static const char *device = "/dev/spidev2.1";

static int32_t  spi_fd;
static uint32_t spi_mode;
static uint8_t  spi_bits = 8;
static uint32_t spi_speed = 800000;
static uint16_t spi_delay;
static int verbose;

static void hex_dump(const void *src, size_t length, size_t line_size, char *prefix)
{
	int i = 0;
	const unsigned char *address = src;
	const unsigned char *line = address;
	unsigned char c;

	printf("%s | ", prefix);
	while (length-- > 0) {
		printf("%02X ", *address++);
		if (!(++i % line_size) || (length == 0 && i % line_size)) {
			if (length == 0) {
				while (i++ % line_size)
					printf("__ ");
			}
			printf(" | ");  /* right close */
			while (line < address) {
				c = *line++;
				printf("%c", (c < 33 || c == 255) ? 0x2E : c);
			}
			printf("\n");
			if (length > 0)
				printf("%s | ", prefix);
		}
	}
}

/*
 *  Unescape - process hexadecimal escape character
 *      converts shell input "\x23" -> 0x23
 */
static int unescape(char *_dst, char *_src, size_t len)
{
	int ret = 0;
	char *src = _src;
	char *dst = _dst;
	unsigned int ch;

	while (*src) {
		if (*src == '\\' && *(src+1) == 'x') {
			sscanf(src + 2, "%2x", &ch);
			src += 4;
			*dst++ = (unsigned char)ch;
		} else {
			*dst++ = *src++;
		}
		ret++;
	}
	return ret;
}

static int transfer(int fd, uint8_t const *tx, uint8_t const *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 = spi_delay,
		.speed_hz = spi_speed,
		.bits_per_word = spi_bits,
        .cs_change = 0,
	};

	if (spi_mode & SPI_TX_QUAD)
		tr.tx_nbits = 4;
	else if (spi_mode & SPI_TX_DUAL)
		tr.tx_nbits = 2;
	if (spi_mode & SPI_RX_QUAD)
		tr.rx_nbits = 4;
	else if (spi_mode & SPI_RX_DUAL)
		tr.rx_nbits = 2;
	if (!(spi_mode & SPI_LOOP)) {
		if (spi_mode & (SPI_TX_QUAD | SPI_TX_DUAL))
			tr.rx_buf = 0;
		else if (spi_mode & (SPI_RX_QUAD | SPI_RX_DUAL))
			tr.tx_buf = 0;
	}

	ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
	if (ret < 1)
		pabort("can't send spi message");

	if (verbose){
		hex_dump(tx, len, 32, "TX");
        hex_dump(rx, len, 32, "RX");
    }
    
    return ret;
}

//向SSD1106写入一个字节。
//dat:要写入的数据/命令
//cmd:数据/命令标志 0,表示命令;1,表示数据;
void OLED_WR_Byte(u8 dat,u8 cmd)
{			  
    u8 tx[2];
    u8 rx[2];
    tx[0] = dat;
    if(cmd)
    {
        system("echo 1 > /sys/class/gpio/gpio1/value");
        transfer(spi_fd,tx,rx,1);
    }
    else
    {
       system("echo 0 > /sys/class/gpio/gpio1/value");
       transfer(spi_fd,tx,rx,1);
    }  
} 

//初始化SSD1306					    
void OLED_Init(void)
{ 	
    //OLED_RST_Set();
	usleep(100000);
	//OLED_RST_Clr();
	usleep(100000);
	//OLED_RST_Set(); 
					  
	OLED_WR_Byte(0xAE,OLED_CMD);//--turn off oled panel
	OLED_WR_Byte(0x02,OLED_CMD);//---set low column address
	OLED_WR_Byte(0x10,OLED_CMD);//---set high column address
	OLED_WR_Byte(0x40,OLED_CMD);//--set start line address  Set Mapping RAM Display Start Line (0x00~0x3F)
	OLED_WR_Byte(0x81,OLED_CMD);//--set contrast control register
	OLED_WR_Byte(0xCF,OLED_CMD); // Set SEG Output Current Brightness
	OLED_WR_Byte(0xA1,OLED_CMD);//--Set SEG/Column Mapping     0xa0左右反置 0xa1正常
	OLED_WR_Byte(0xC8,OLED_CMD);//Set COM/Row Scan Direction   0xc0上下反置 0xc8正常
	OLED_WR_Byte(0xA6,OLED_CMD);//--set normal display
	OLED_WR_Byte(0xA8,OLED_CMD);//--set multiplex ratio(1 to 64)
	OLED_WR_Byte(0x3f,OLED_CMD);//--1/64 duty
	OLED_WR_Byte(0xD3,OLED_CMD);//-set display offset	Shift Mapping RAM Counter (0x00~0x3F)
	OLED_WR_Byte(0x00,OLED_CMD);//-not offset
	OLED_WR_Byte(0xd5,OLED_CMD);//--set display clock divide ratio/oscillator frequency
	OLED_WR_Byte(0x80,OLED_CMD);//--set divide ratio, Set Clock as 100 Frames/Sec
	OLED_WR_Byte(0xD9,OLED_CMD);//--set pre-charge period
	OLED_WR_Byte(0xF1,OLED_CMD);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
	OLED_WR_Byte(0xDA,OLED_CMD);//--set com pins hardware configuration
	OLED_WR_Byte(0x12,OLED_CMD);
	OLED_WR_Byte(0xDB,OLED_CMD);//--set vcomh
	OLED_WR_Byte(0x40,OLED_CMD);//Set VCOM Deselect Level
	OLED_WR_Byte(0x20,OLED_CMD);//-Set Page Addressing Mode (0x00/0x01/0x02)
	OLED_WR_Byte(0x02,OLED_CMD);//
	OLED_WR_Byte(0x8D,OLED_CMD);//--set Charge Pump enable/disable
	OLED_WR_Byte(0x14,OLED_CMD);//--set(0x10) disable
	OLED_WR_Byte(0xA4,OLED_CMD);// Disable Entire Display On (0xa4/0xa5)
	OLED_WR_Byte(0xA6,OLED_CMD);// Disable Inverse Display On (0xa6/a7) 
	OLED_WR_Byte(0xAF,OLED_CMD);//--turn on oled panel
	
	OLED_WR_Byte(0xAF,OLED_CMD); /*display ON*/ 
	OLED_Clear();
	OLED_Set_Pos(0,0); 	
}

void OLED_Set_Pos(unsigned char x, unsigned char y) 
{ 
	OLED_WR_Byte(0xb0+y,OLED_CMD);
	OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD);
	OLED_WR_Byte((x&0x0f)|0x01,OLED_CMD); 
}   	  
//开启OLED显示    
void OLED_Display_On(void)
{
	OLED_WR_Byte(0X8D,OLED_CMD);  //SET DCDC命令
	OLED_WR_Byte(0X14,OLED_CMD);  //DCDC ON
	OLED_WR_Byte(0XAF,OLED_CMD);  //DISPLAY ON
}
//关闭OLED显示     
void OLED_Display_Off(void)
{
	OLED_WR_Byte(0X8D,OLED_CMD);  //SET DCDC命令
	OLED_WR_Byte(0X10,OLED_CMD);  //DCDC OFF
	OLED_WR_Byte(0XAE,OLED_CMD);  //DISPLAY OFF
}		   			 
//清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!	  
void OLED_Clear(void)  
{  
	u8 i,n;		    
	for(i=0;i<8;i++)  
	{  
		OLED_WR_Byte (0xb0+i,OLED_CMD);    //设置页地址(0~7)
		OLED_WR_Byte (0x02,OLED_CMD);      //设置显示位置—列低地址
		OLED_WR_Byte (0x10,OLED_CMD);      //设置显示位置—列高地址   
		for(n=0;n<128;n++)OLED_WR_Byte(0,OLED_DATA); 
	} //更新显示
}


//在指定位置显示一个字符,包括部分字符
//x:0~127
//y:0~63
//mode:0,反白显示;1,正常显示				 
//size:选择字体 16/12 
void OLED_ShowChar(u8 x,u8 y,u8 chr)
{      	
	unsigned char c=0,i=0;	
		c=chr-' ';//得到偏移后的值			
		if(x>Max_Column-1){x=0;y=y+2;}
		if(SIZE ==16)
			{
			OLED_Set_Pos(x,y);	
			for(i=0;i<8;i++)
			OLED_WR_Byte(F8X16[c*16+i],OLED_DATA);
			OLED_Set_Pos(x,y+1);
			for(i=0;i<8;i++)
			OLED_WR_Byte(F8X16[c*16+i+8],OLED_DATA);
			}
			else {	
				OLED_Set_Pos(x,y+1);
				for(i=0;i<6;i++)
				OLED_WR_Byte(F6x8[c][i],OLED_DATA);
				
			}
}
//m^n函数
u32 oled_pow(u8 m,u8 n)
{
	u32 result=1;	 
	while(n--)result*=m;    
	return result;
}				  
//显示2个数字
//x,y :起点坐标	 
//len :数字的位数
//size:字体大小
//mode:模式	0,填充模式;1,叠加模式
//num:数值(0~4294967295);	 		  
void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size)
{         	
	u8 t,temp;
	u8 enshow=0;						   
	for(t=0;t<len;t++)
	{
		temp=(num/oled_pow(10,len-t-1))%10;
		if(enshow==0&&t<(len-1))
		{
			if(temp==0)
			{
				OLED_ShowChar(x+(size/2)*t,y,' ');
				continue;
			}else enshow=1; 
		 	 
		}
	 	OLED_ShowChar(x+(size/2)*t,y,temp+'0'); 
	}
} 
//显示一个字符号串
void OLED_ShowString(u8 x,u8 y,u8 *chr)
{
	unsigned char j=0;
	while (chr[j]!='\0')
	{		OLED_ShowChar(x,y,chr[j]);
			x+=8;
		if(x>120){x=0;y+=2;}
			j++;
	}
}
//显示汉字
void OLED_ShowCHinese(u8 x,u8 y,u8 no)
{      			    
	u8 t,adder=0;
	OLED_Set_Pos(x,y);	
    for(t=0;t<16;t++)
		{
				OLED_WR_Byte(Hzk[2*no][t],OLED_DATA);
				adder+=1;
     }	
		OLED_Set_Pos(x,y+1);	
    for(t=0;t<16;t++)
			{	
				OLED_WR_Byte(Hzk[2*no+1][t],OLED_DATA);
				adder+=1;
      }					
}
/***********功能描述:显示显示BMP图片128×64起始点坐标(x,y),x的范围0~127,y为页的范围0~7*****************/
void OLED_DrawBMP(unsigned char x0, unsigned char y0,unsigned char x1, unsigned char y1,unsigned char BMP[])
{ 	
 unsigned int j=0;
 unsigned char x,y;
  
  if(y1%8==0) y=y1/8;      
  else y=y1/8+1;
	for(y=y0;y<y1;y++)
	{
		OLED_Set_Pos(x0,y);
    for(x=x0;x<x1;x++)
	    {      
	    	OLED_WR_Byte(BMP[j++],OLED_DATA);	    	
	    }
	}
} 

int spidev_init()
{
    int ret = 0;
    spi_mode = SPI_MODE_0;
    verbose = 0;

	spi_fd = open(device, O_RDWR);
	if (spi_fd < 0)
		pabort("can't open device");

	/*
	 * spi mode
	 */
	ret = ioctl(spi_fd, SPI_IOC_WR_MODE32, &spi_mode);
	if (ret == -1)
		pabort("can't set spi mode");

	ret = ioctl(spi_fd, SPI_IOC_RD_MODE32, &spi_mode);
	if (ret == -1)
		pabort("can't get spi mode");

	/*
	 * bits per word
	 */
	ret = ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bits);
	if (ret == -1)
		pabort("can't set bits per word");

	ret = ioctl(spi_fd, SPI_IOC_RD_BITS_PER_WORD, &spi_bits);
	if (ret == -1)
		pabort("can't get bits per word");

	/*
	 * max speed hz
	 */
	ret = ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &spi_speed);
	if (ret == -1)
		pabort("can't set max speed hz");

	ret = ioctl(spi_fd, SPI_IOC_RD_MAX_SPEED_HZ, &spi_speed);
	if (ret == -1)
		pabort("can't get max speed hz");

	printf("spi mode: 0x%x\n", spi_mode);
	printf("bits per word: %d\n", spi_bits);
	printf("max speed: %d Hz (%d KHz)\n", spi_speed, spi_speed/1000);

	return ret;
}

 测试使用

int main(int argc, char *argv[])
{
    //导出DC口,这里使用的是GPIO1管脚,作为DC口使用(命令数据选择管脚)
    system("echo 1 > /sys/class/gpio/export");
    system("echo out >/sys/class/gpio/gpio1/direction");
    
    spidev_init();
    OLED_Init();
    
    OLED_ShowString(0,0,"hello world");

    return 0;
}

 注意,这里使用了一种偷懒的做法,直接导出了一个GPIO1管脚使用,通过system调用的方式先导出IO口。在OLED的发送接口里,也通过了system命令调用的方式(相当低效且费资源,正式用的话肯定不这么做,这里仅是为了测试)。

gpiochipxx:当前SoC 所包含的GPIO 控制器,I.MX6UL/I.MX6ULL 一共包含了5 个GPIO控制器,分别为GPIO1、GPIO2、GPIO3、GPIO4、GPIO5,在这里分别对应gpiochip0、gpiochip32、gpiochip64、gpiochip96、gpiochip128 这5 个文件夹,每一个gpiochipxx 文件夹用来管理一组GPIO。

 例如要导出GPIO1_1,可以通过这种方式:

echo 1 > /sys/class/gpio/export

对于给定的一个GPIO 引脚,如何计算它在sysfs 中对应的编号呢?其实非常简单,譬如给定一个GPIO引脚为GPIO4_IO16,那它对应的编号是多少呢?首先我们要确定GPIO4 对应于gpiochip96,该组GPIO 引脚的最小编号是96(对应于GPIO4_IO0),所以GPIO4_IO16 对应的编号自然是96 + 16 = 112;同理GPIO3_IO20 对应的编号是64 + 20 = 84。

//向SSD1106写入一个字节。
//dat:要写入的数据/命令
//cmd:数据/命令标志 0,表示命令;1,表示数据;
void OLED_WR_Byte(u8 dat,u8 cmd)
{			  
    u8 tx[2];
    u8 rx[2];
    tx[0] = dat;
    if(cmd)
    {
        system("echo 1 > /sys/class/gpio/gpio1/value");
        transfer(spi_fd,tx,rx,1);
    }
    else
    {
       system("echo 0 > /sys/class/gpio/gpio1/value");
       transfer(spi_fd,tx,rx,1);
    }  
}

使用 echo 命令通过文件系统导出和控制 GPIO 口是一种简单易用的方式,但在性能要求较高的场景下可能不够高效。可以考虑使用 GPIO 库、设备驱动程序或用户空间库和工具来提高效率,如可以使用libgpiod库的方式。 

libgpiod库简介

libgpiod是用于与linux GPIO交互的C库和工具,从 linux 4.8 后,官方不推荐使用 GPIO sysfs 接口,libgpiod库封装了 ioctl 调用和简单的API接口。Libgpiod是一种字符设备接口,GPIO访问控制是通过操作字符设备文件(比如/dev/gpiodchip0)实现的。
与sysfs方式相比,libgpiod可以保证所有分配的资源,在关闭文件描述符后得到完全释放,并且拥有sysfs方式接口中不存在的功能(如时间轮询,一次设置/读取多个gpio值)。此外libgpiod还包含一组命令行工具,允许用户使用脚本对gpio进行个性化操作。

详细介绍:libgpiod/libgpiod.git - C library and tools for interacting with the linux GPIO character device

libgpiod库的简单使用:

#include <gpiod.h>
#include <stdio.h>
 int main() {
    struct gpiod_chip *chip;
    struct gpiod_line *line;
    int value;
     // 打开 GPIO 控制器
    chip = gpiod_chip_open("/dev/gpiochip0");
    if (!chip) {
        perror("Failed to open GPIO chip");
        return -1;
    }
     // 获取 GPIO 口
    line = gpiod_chip_get_line(chip, 17);
    if (!line) {
        perror("Failed to get GPIO line");
        gpiod_chip_close(chip);
        return -1;
    }
     // 设置 GPIO 口为输出模式
    int ret = gpiod_line_request_output(line, "example", 0);
    if (ret < 0) {
        perror("Failed to set GPIO line as output");
        gpiod_line_release(line);
        gpiod_chip_close(chip);
        return -1;
    }
     // 控制 GPIO 输出高低电平
    ret = gpiod_line_set_value(line, 1);
    if (ret < 0) {
        perror("Failed to set GPIO line value");
        gpiod_line_release(line);
        gpiod_chip_close(chip);
        return -1;
    }
     // 读取 GPIO 输入值
    value = gpiod_line_get_value(line);
    printf("GPIO value: %d\n", value);
     // 释放资源
    gpiod_line_release(line);
    gpiod_chip_close(chip);
     return 0;
}

上述示例中,首先通过 gpiod_chip_open 打开指定的 GPIO 控制器(例如 /dev/gpiochip0 ),然后使用 gpiod_chip_get_line 获取指定的 GPIO 口(例如 17 号口)。接下来,我们使用 gpiod_line_request_output 将 GPIO 口设置为输出模式,并使用 gpiod_line_set_value 控制输出高低电平。最后,我们使用 gpiod_line_get_value 读取 GPIO 输入值,并使用 gpiod_line_releasegpiod_chip_close 释放资源。 需要注意的是,使用 libgpiod 需要安装相应的库文件和头文件,并在编译时链接 libgpiod 库。

需要注意的是,libgpiod 要求 Linux 内核版本至少为 4.8,因为它依赖于内核的 GPIO 字符设备接口(gpiochip)。在 4.8 版本之前的内核中,该接口可能不存在或不完整,因此无法使用 libgpiod 库。可以使用如下的方式:

int main(int argc, char *argv[])
{
    //导出DC口,这里使用的是GPIO1管脚,作为DC口使用(命令数据选择管脚)
    //system("echo 1 > /sys/class/gpio/export");
    //system("echo out >/sys/class/gpio/gpio1/direction");
    dc_p = fopen("/sys/class/gpio/export","w");
    fprintf(dc_p,"%d",1);
    fclose(dc_p);
    dc_p = fopen("/sys/class/gpio/gpio1/direction","w");
    fprintf(dc_p,"out");
    fclose(dc_p);
    dc_p = fopen("/sys/class/gpio/gpio1/value","w");
    fprintf(dc_p,"1");
    fflush(dc_p);
    
    spidev_init();
    OLED_Init();
    
    OLED_ShowString(0,0,"hello world");

    return 0;
}
//向SSD1106写入一个字节。
//dat:要写入的数据/命令
//cmd:数据/命令标志 0,表示命令;1,表示数据;
void OLED_WR_Byte(u8 dat,u8 cmd)
{			  
    u8 tx[2];
    u8 rx[2];
    tx[0] = dat;
    if(cmd)
    {
        //system("echo 1 > /sys/class/gpio/gpio1/value");
        fprintf(dc_p,"1");
        fflush(dc_p);
        transfer(spi_fd,tx,rx,1);
    }
    else
    {
       //system("echo 0 > /sys/class/gpio/gpio1/value");
       fprintf(dc_p,"0");
       fflush(dc_p);
       transfer(spi_fd,tx,rx,1);
    }  
} 

也可以使用open系统调用函数。究竟哪个更高效?需要考虑到多个因素,包括数据量、缓冲区大小、系统调用次数等。以上推荐使用fopen和fwrite.

fopenopen 是 Linux 中用于打开文件的两个函数,它们之间有一些区别。

1. fopen 是 C 标准库中的函数,用于以流的形式打开文件。它返回一个 FILE* 类型的指针,可以使用标准库函数(如 freadfwritefprintf 等)对文件进行读写操作。 fopen 函数提供了一些方便的功能,如自动缓冲、格式化输入输出等。但是,由于它是标准库函数,因此在处理大量数据时可能会有性能上的损失。

2. open 是系统调用函数,用于以文件描述符的形式打开文件。它返回一个整数类型的文件描述符,可以使用系统调用函数(如 readwriteioctl 等)对文件进行读写操作。 open 函数是操作系统提供的原始接口,直接与内核交互,因此在处理大量数据时通常比 fopen 更高效。 总的来说,如果你只是进行简单的文件读写操作,并且希望使用标准库函数进行处理,那么可以选择使用 fopen 。但是,如果你需要更高效的文件操作,并且需要使用系统调用函数进行底层控制,那么可以选择使用 open 。 

fwrite 是 C 标准库中的函数,用于将数据写入文件。它会先将数据写入缓冲区,然后再将缓冲区的数据写入文件。相比之下, write 是一个系统调用,直接将数据写入文件,不经过缓冲区。 对于小数据量的写入操作, fwrite 的效率可能会更高,因为它可以将多个小数据一次性写入缓冲区,然后一次性写入文件,减少了系统调用的次数。 而对于大数据量的写入操作, write 的效率可能更高。因为 fwrite 需要将数据先写入缓冲区,然后再写入文件,这个过程可能会涉及到多次的缓冲区刷新操作。而 write 直接将数据写入文件,减少了缓冲区刷新的开销。 此外,还需要考虑到缓冲区大小的影响。如果缓冲区大小适合数据量, fwrite 可能会有更好的性能。但是如果数据量超过了缓冲区大小, fwrite 需要多次刷新缓冲区,可能导致性能下降。 总的来说,对于小数据量的写入操作, fwrite 可能更高效。而对于大数据量的写入操作, write 可能更高效。但是具体的效率还需要根据具体的场景和需求进行测试和评估。

使用系统调用的方式如下:

int main(int argc, char *argv[])
{
    //导出DC口,这里使用的是GPIO1管脚,作为DC口使用(命令数据选择管脚)
    //system("echo 1 > /sys/class/gpio/export");
    //system("echo out >/sys/class/gpio/gpio1/direction");
    system("echo 1 > /sys/class/gpio/unexport");
    ssize_t bytes_written;
    const char* value = "1";

    // 打开 /sys/class/gpio/export 文件
    fd_dc = open("/sys/class/gpio/export", O_WRONLY,0644);
    if (fd_dc == -1) {
        // 处理打开文件失败的情况
        printf("open error\n");
        return -1;
    }
    bytes_written = write(fd_dc, "1", 1);
    close(fd_dc);
    if (bytes_written == -1) {
        // 处理写入文件失败的情况
        printf("write failed: %s\n", strerror(errno));
        return -1;
    }

    // 打开 /sys/class/gpio/gpio1/direction 文件
    fd_dc = open("/sys/class/gpio/gpio1/direction", O_WRONLY,0644);
    if (fd_dc == -1) {
        // 处理打开文件失败的情况
        printf("open error1\n");
        return -1;
    }
    bytes_written = write(fd_dc, "out", 3);
    close(fd_dc);
    if (bytes_written == -1) {
        // 处理写入文件失败的情况
        printf("write failed1: %s\n", strerror(errno));
        return -1;
    }

    // 打开 /sys/class/gpio/gpio1/value 文件
    fd_dc = open("/sys/class/gpio/gpio1/value", O_WRONLY,0644);
    if (fd_dc == -1) {
        // 处理打开文件失败的情况
        printf("open error2\n");
        return -1;
    }
    bytes_written = write(fd_dc, value, strlen(value));
    fsync(fd_dc);
    
    spidev_init();
    OLED_Init();
    
    OLED_ShowString(0,0,"hello world");

    return 0;
}

在调用 open 函数时,可以通过第二个参数指定文件的访问权限。权限参数是一个八进制数,表示文件的读、写和执行权限。 以下是一些常用的权限参数值:

- O_RDONLY :只读模式,表示打开文件以供读取。

- O_WRONLY :只写模式,表示打开文件以供写入。

- O_RDWR :读写模式,表示打开文件以供读取和写入。

- O_CREAT :如果文件不存在,则创建文件。

- O_EXCL :与 O_CREAT 一起使用,如果文件已经存在,则返回错误。

- O_TRUNC :如果文件存在,并且以写入模式打开,则将文件截断为零长度。

- O_APPEND :以追加模式打开文件,即写入时将数据追加到文件末尾。 权限参数可以与上述标志位进行位运算,以指定文件的访问权限。例如,若要以读写模式打开文件并在文件不存在时创建它,可以使用以下权限参数:

int fd = open("filename.txt", O_RDWR | O_CREAT, 0644);

在上述示例中, 0644 是一个八进制数,表示文件的权限。其中, 0 表示八进制数, 6 表示用户(文件所有者)具有读写权限, 4 表示组用户具有只读权限, 4 表示其他用户具有只读权限。 需要注意的是,权限参数只在创建文件时起作用,对于已经存在的文件,权限参数不会改变文件的权限。文件的实际权限由文件系统的权限控制决定。

最后,为了测试OLED,写一段简单的makefile,方便编译。

# test spidev-oled
#
# Copyright (C) 2023 yangyongzhen <5234117529@qq.com>
#

CC	?= arm-linux-gnueabihf-gcc
AR	?= arm-linux-gnueabihf-ar
STRIP	?= strip

CFLAGS		?= -O2
# When debugging, use the following instead
#CFLAGS		:= -O -g
CFLAGS		+= -Wall
SOCFLAGS	:= -fpic -D_REENTRANT $(CFLAGS)

#KERNELVERSION	:= $(shell uname -r)

.PHONY: all strip clean 

all:
	$(CC) spidev_oled.c -o spidev_oled
  
clean:
	rm -rf *.o 

测试demo:

int main(int argc, char *argv[])
{
    //导出DC口,这里使用的是GPIO1管脚,作为DC口使用(命令数据选择管脚)
    //system("echo 1 > /sys/class/gpio/export");
    //system("echo out >/sys/class/gpio/gpio1/direction");
    u8 t;
    dc_p = fopen("/sys/class/gpio/export","w");
    fprintf(dc_p,"%d",1);
    fclose(dc_p);
    dc_p = fopen("/sys/class/gpio/gpio1/direction","w");
    fprintf(dc_p,"out");
    fclose(dc_p);
    dc_p = fopen("/sys/class/gpio/gpio1/value","w");
    fprintf(dc_p,"1");
    fflush(dc_p);
    
    spidev_init();
    OLED_Init();
    
    while(1) 
	{		
		OLED_Clear();
		OLED_ShowCHinese(0,0,0);//中
		OLED_ShowCHinese(18,0,1);//景
		OLED_ShowCHinese(36,0,2);//园
		OLED_ShowCHinese(54,0,3);//电
		OLED_ShowCHinese(72,0,4);//子
		OLED_ShowCHinese(90,0,5);//科
		OLED_ShowCHinese(108,0,6);//技
		OLED_ShowString(0,3,"1.3' OLED TEST");
		//OLED_ShowString(8,2,"ZHONGJINGYUAN");  
	 //	OLED_ShowString(20,4,"2014/05/01");  
		OLED_ShowString(0,6,"ASCII:");  
		OLED_ShowString(63,6,"CODE:");  
		OLED_ShowChar(48,6,t);//显示ASCII字符	   
		t++;
		if(t>'~')t=' ';
		OLED_ShowNum(103,6,t,3,16);//显示ASCII字符的码值 	
			
		
		delay_ms(8000);
		OLED_Clear();
		delay_ms(8000);
		OLED_DrawBMP(0,0,128,8,BMP1);  //图片显示(图片显示慎用,生成的字表较大,会占用较多空间,FLASH空间8K以下慎用)
		delay_ms(8000);
		OLED_DrawBMP(0,0,128,8,BMP2);
		delay_ms(8000);
	}	  

}

   

 工程完整源码下载地址:

https://download.csdn.net/download/qq8864/88117562

其他资源

嵌入式Linux——IIC总线驱动(3):IIC驱动OLED外设_iic怎么唤醒外设_moxue10的博客-CSDN博客

Linux系统GPIO应用编程_linux控制gpio程序_行稳方能走远的博客-CSDN博客

linux系统基于syfs控制gpio_gpio linux sys_嵌入式Linux开发的博客-CSDN博客

交叉编译开源代码(以libgpiod为例)_libgpiod交叉编译_仰望&南极光的博客-CSDN博客

[imx6ull应用开发]GPIO编程之LED灯设备控制---sysfs方式和libgpiod方式_gpiod_line_request_output_WH^2的博客-CSDN博客

飞凌嵌入式技术帖——i.MX9352的GPIO怎么用?_dts_设备_操作

[imx6ull应用开发]GPIO编程之LED灯设备控制---sysfs方式和libgpiod方式_gpiod_line_request_output_WH^2的博客-CSDN博客

C语言方式(libgpiod) - Sipeed Wiki

Libgpiod库的使用,点亮LED_猪突猛进进进的博客-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/834004.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

泛微最近的漏洞利用工具

WeaverExploit_All 0x01 介绍 泛微最近的漏洞利用工具&#xff08;PS&#xff1a;2023&#xff09; 集成了QVD-2023-5012、CVE-2023-2523、CVE-2023-2648、getloginid_ofsLogin 漏洞利用 新增&#xff1a;WorkflowServiceXml 内存马注入、uploaderOperate文件上传漏洞、Del…

go-zero超强工具goctl的常用命令api,rpc,model及其构建的服务解析

goctl api 详情移步&#xff1a; go-zero的路由机制解析 基于go-zero的api服务刨析并对比与gin的区别 goctl rpc goctl支持多种rpc&#xff0c;较为流行的是google开源的grpc&#xff0c;这里主要介绍goctl rpc protoc的代码生成与使用。 protoc是grpc的命令&#xff0c;作用…

SpringBoot的pom文件、容器、组件

一、pom文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4…

springboot-mybatis的增删改查

目录 一、准备工作 二、常用配置 三、尝试 四、增删改查 1、增加 2、删除 3、修改 4、查询 五、XML的映射方法 一、准备工作 实施前的准备工作&#xff1a; 准备数据库表 创建一个新的springboot工程&#xff0c;选择引入对应的起步依赖&#xff08;mybatis、mysql驱动…

基于springboot的课程作业管理系统【附开题|ppt|万字文档(LW)和搭建文档】

主要功能 学生登录&#xff1a; ①首页、个人中心&#xff1a;修改密码、个人信息管理等 ②公告信息管理、课程信息管理、学生选课管理、作业布置管理、作业提交管理、作业评分管理、课程评价管理、课程资源管理 教师登录&#xff1a; ①首页、个人中心&#xff1a;修改密码、…

操作系统专栏4-网络专题from小林coding

网络专题 文件传输mmapwritesend file大文件传输过程 文件传输 传统的文件传输过程 在这个过程中发生了4次用户态与内核态之间的切换,4次数据拷贝分别是 read系统调用陷入内核,read完成返回write调用陷入内核,write返回 4次数据拷贝分别是 磁盘->内核缓冲区->用户缓冲…

改进正弦算法引导的蜣螂优化算法(MSADBO)

概述 蜣螂优化算法由于其寻优速度和收敛精度&#xff0c;自2023年问世以来&#xff0c;热度一直很高。本篇文章对蜣螂算法进行改进&#xff0c;改进思路是参考2023年6月25号发表在知网的一篇文献&#xff08;文献放在了文章末尾&#xff09;。 改进的蜣螂优化算法融合了改进的正…

Redis 变慢了 解决方案

一、Redis为什么变慢了 1.Redis真的变慢了吗&#xff1f; 对 Redis 进行基准性能测试 例如&#xff0c;我的机器配置比较低&#xff0c;当延迟为 2ms 时&#xff0c;我就认为 Redis 变慢了&#xff0c;但是如果你的硬件配置比较高&#xff0c;那么在你的运行环境下&#xff…

Cloud Keys Delphi Edition Crack

Cloud Keys Delphi Edition Crack 云密钥使基于云的密钥和秘密管理与任何支持的平台或开发技术轻松集成。这些易于使用的组件可用于与流行的云密钥管理提供商安全集成&#xff0c;如Amazon KMS、Amazon AWS Secrets、Azure key Vault、Google cloud KMS和Google Secret Manager…

自动化测试工具在软件测试中扮演了重要的角色

随着软件开发行业的发展和变革&#xff0c;软件测试变得愈发重要。在传统的软件测试过程中&#xff0c;测试人员需要手动执行测试用例&#xff0c;耗费大量时间和资源。然而&#xff0c;随着自动化测试工具的出现&#xff0c;软件测试的效率和质量得到了极大的提升。 自动化测试…

用Python自制软件,看视频畅通无阻

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 一个账号只能登录一台设备&#xff1f;涨价就涨价&#xff0c;至少还能借借朋友的&#xff0c;谁还没几个朋友&#xff0c;搞限制登录这一出&#xff0c;瞬间不稀罕了 这个年头谁还不会点技术了&#xff0c;直接拿python自制一…

Excel中——日期列后添加星期

需求&#xff1a;在日期列中添加星期几&#xff1f; 第一步&#xff1a;打开需要添加星期的Excel文件&#xff0c;在日期后面添加日期 第二步&#xff1a;选择日期列&#xff0c;点击鼠标右键&#xff0c;在下拉列表中&#xff0c;选择“设置单元格格式” 第三步&#xff1a; 在…

Miniled透明屏:超薄、轻便,还有哪些特点?

Miniled透明屏是一种新型的显示屏技术&#xff0c;它采用了微小的LED灯珠作为显示单元&#xff0c;通过透明的材料进行封装&#xff0c;使得整个屏幕具有透明的特性。Miniled透明屏具有以下几个特点&#xff1a; 首先&#xff0c;Miniled透明屏具有高亮度和高对比度的特点。 由…

TypeScript实战篇 - TS实战: 服务层开发-令牌

目录 接口说明 服务设计 WHY NOT Socket&#xff1f; huatian/svc【node.js 接口服务端】 huatian-svc/package.json huatian-svc/tsconfig.json huatian-svc/src/main.ts huatian-svc/nodemon.json huatian-svc/src/context/AccountContext.ts huatian-svc/src/repo…

软件测试面试真题 | 什么是PO设计模式?

面试官问&#xff1a;UI自动化测试中有使用过设计模式吗&#xff1f;了解什么是PO设计模式吗&#xff1f; 考察点 《page object 设计模式》&#xff1a;PageObject设计模式的设计思想、设计原则 《web自动化测试实战》&#xff1a;结合PageObject在真实项目中的实践与应用情…

活动目录密码更改

定期更改密码是一种健康的习惯&#xff0c;因为它有助于阻止使用被盗凭据的网络攻击&#xff0c;安全专家建议管理员应确保用户使用有效的密码过期策略更改其密码。 管理员可以通过电子邮件通知用户在密码即将过期时更改其密码&#xff0c;但在许多组织中&#xff0c;用户只能…

举个栗子!Tableau 技巧(256):灵活折叠文本表的多级数据行

通常&#xff0c;Tableau 默认的图表分层结构是统一打开或关上&#xff0c;有什么办法可以按需选择展开或折叠&#xff1f;如下示例&#xff1a;单击“”展开层级&#xff0c;单击“-“收起层级。 可以试试集操作&#xff01;今天的栗子&#xff0c;就来分享具体实现方法吧~ 本…

解读Spring的context:property-placeholder

在spring中&#xff0c;如果要给程序定义一些参数&#xff0c;可以放在application.properties中&#xff0c;通过<context:property-placeholder>加载这个属性文件&#xff0c;然后就可以通过value给我们的变量自动赋值&#xff0c;如果你们的程序可能运行在多个环境中&…

什么是头脑风暴法,有哪些原则?

1. 什么是头脑风暴法&#xff1f; 头脑风暴法&#xff08;Brainstorming&#xff09;是一种用于创造性思维和问题解决的方法。它旨在通过集体讨论和思维碰撞&#xff0c;激发团队成员的创造力和想象力&#xff0c;从而产生新的创意和解决方案。 在头脑风暴会议中&#xff…

数据结构 10-排序4 统计工龄 桶排序/计数排序(C语言)

给定公司名员工的工龄&#xff0c;要求按工龄增序输出每个工龄段有多少员工。 输入格式: 输入首先给出正整数&#xff08;≤&#xff09;&#xff0c;即员工总人数&#xff1b;随后给出个整数&#xff0c;即每个员工的工龄&#xff0c;范围在[0, 50]。 输出格式: 按工龄的递…