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

周日业余时间太无聊,又不喜欢玩游戏,大家的兴趣爱好都是啥?我觉得敲代码也是一种兴趣爱好。正巧手边有一块儿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/目录下。

bash 复制代码
&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

cpp 复制代码
/* 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

编译内核和设备树

bash 复制代码
#加载环境
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设备:
cpp 复制代码
int spi_fd;
spi_fd = open("/dev/spidev0.0", O_RDWR);
if (spi_fd < 0) {
    perror("Failed to open SPI device");
    return -1;
}
  1. 设置SPI模式、速度和位数:
cpp 复制代码
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;
}
  1. 创建spi_ioc_message结构体,并设置相关字段:
cpp 复制代码
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表示每次传输前会拉低片选信号,传输完成后会拉高片选信号。

  1. 发送命令和数据:
cpp 复制代码
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驱动接口封装

cpp 复制代码
#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;
}

测试使用

cpp 复制代码
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,可以通过这种方式:

bash 复制代码
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。

cpp 复制代码
//向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库的简单使用:

cpp 复制代码
#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 库。可以使用如下的方式:

cpp 复制代码
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;
}
cpp 复制代码
//向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 可能更高效。但是具体的效率还需要根据具体的场景和需求进行测试和评估。

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

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

cpp 复制代码
int fd = open("filename.txt", O_RDWR | O_CREAT, 0644);

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

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

bash 复制代码
# test spidev-oled
#
# Copyright (C) 2023 yangyongzhen <[email protected]>
#

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:

cpp 复制代码
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博客

相关推荐
Johny_Zhao3 小时前
基于CentOS Stream 8的物联网平台深度优化方案
linux·网络·网络安全·信息安全·云计算·shell·yum源·系统运维
Jooolin10 小时前
【Linux】虚拟机、服务器、双系统,谁才是 Ubuntu 的最佳方案?
linux·ubuntu·ai编程
K·Herbert15 小时前
最新CentOS 7 yum源失效的解决方案(2025年6月)
linux·运维·centos
别骂我h16 小时前
部署KVM虚拟化平台
linux·运维·服务器
繢鴻16 小时前
紧急救援!Ubuntu崩溃修复大赛
linux·服务器·ubuntu
showmethetime16 小时前
优化nginx参数(基本通用参数)
运维·nginx
老六ip加速器16 小时前
获取ip地址安全吗?如何获取静态ip地址隔离ip
运维·网络·智能路由器
物联网嵌入式小冉学长17 小时前
10.C S编程错误分析
c语言·stm32·单片机·算法·嵌入式
净心净意1 天前
浅谈DaemonSet
运维·jenkins
Apex Predator1 天前
jenkins流水线打包vue无权限
运维·jenkins