嵌入式linux之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/目录下。
&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 panelOLED_WR_Byte(0x02,OLED_CMD);//---set low column addressOLED_WR_Byte(0x10,OLED_CMD);//---set high column addressOLED_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 registerOLED_WR_Byte(0xCF,OLED_CMD); // Set SEG Output Current BrightnessOLED_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 displayOLED_WR_Byte(0xA8,OLED_CMD);//--set multiplex ratio(1 to 64)OLED_WR_Byte(0x3f,OLED_CMD);//--1/64 dutyOLED_WR_Byte(0xD3,OLED_CMD);//-set display offset Shift Mapping RAM Counter (0x00~0x3F)OLED_WR_Byte(0x00,OLED_CMD);//-not offsetOLED_WR_Byte(0xd5,OLED_CMD);//--set display clock divide ratio/oscillator frequencyOLED_WR_Byte(0x80,OLED_CMD);//--set divide ratio, Set Clock as 100 Frames/SecOLED_WR_Byte(0xD9,OLED_CMD);//--set pre-charge periodOLED_WR_Byte(0xF1,OLED_CMD);//Set Pre-Charge as 15 Clocks & Discharge as 1 ClockOLED_WR_Byte(0xDA,OLED_CMD);//--set com pins hardware configurationOLED_WR_Byte(0x12,OLED_CMD);OLED_WR_Byte(0xDB,OLED_CMD);//--set vcomhOLED_WR_Byte(0x40,OLED_CMD);//Set VCOM Deselect LevelOLED_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/disableOLED_WR_Byte(0x14,OLED_CMD);//--set(0x10) disableOLED_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 panelOLED_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 ONOLED_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 OFFOLED_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_release
和 gpiod_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.
fopen
和 open
是 Linux 中用于打开文件的两个函数,它们之间有一些区别。
1. fopen
是 C 标准库中的函数,用于以流的形式打开文件。它返回一个 FILE*
类型的指针,可以使用标准库函数(如 fread
、 fwrite
、 fprintf
等)对文件进行读写操作。 fopen
函数提供了一些方便的功能,如自动缓冲、格式化输入输出等。但是,由于它是标准库函数,因此在处理大量数据时可能会有性能上的损失。
2. open
是系统调用函数,用于以文件描述符的形式打开文件。它返回一个整数类型的文件描述符,可以使用系统调用函数(如 read
、 write
、 ioctl
等)对文件进行读写操作。 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 ?= stripCFLAGS ?= -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_oledclean: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博客
相关文章:
![](https://img-blog.csdnimg.cn/6a0c9832919e4a7794f76f45036f119b.png)
嵌入式linux之OLED显示屏SPI驱动实现(SH1106,ssd1306)
周日业余时间太无聊,又不喜欢玩游戏,大家的兴趣爱好都是啥?我觉得敲代码也是一种兴趣爱好。正巧手边有一块儿0.96寸的OLED显示屏,一直在吃灰,何不把玩一把?于是说干就干,最后在我的imax6ul的lin…...
![](https://img-blog.csdnimg.cn/a1c8c1ab19524264a2c8bfb2bcf296d7.png)
关于element ui 安装失败的问题解决方法、查看是否安装成功及如何引入
Vue2引入 执行npm i element-ui -S报错 原因:npm版本太高 报错信息: 解决办法: 使用命令: npm install --legacy-peer-deps element-ui --save 引入: 在main.js文件中引入 //引入Vue import Vue from vue; //引入…...
![](https://img-blog.csdnimg.cn/9c927a2109b043ce9f0ca2a0ea16fd74.jpeg)
Selenium多浏览器处理
Python 版本 #导入依赖 import os from selenium import webdriverdef test_browser():#使用os模块的getenv方法来获取声明环境变量browserbrowser os.getenv("browser").lower()#判断browser的值if browser "headless":driver webdriver.PhantomJS()e…...
![](https://img-blog.csdnimg.cn/2d31d628a4c548139dac8dbd888402aa.jpeg#pic_center)
浅谈 AI 大模型的崛起与未来展望:马斯克的 xAI 与中国产业发展
文章目录 💬话题📋前言🎯AI 大模型的崛起🎯中国 AI 产业的进展与挑战🎯AI 大模型的未来展望🧩补充 📝最后 💬话题 北京时间 7 月 13 日凌晨,马斯克在 Twiiter 上宣布&am…...
![](https://img-blog.csdnimg.cn/2e9431db6aa04a29b1b3a2172537c6fa.png)
【CesiumJS材质】(1)圆扩散
效果示例 最佳实践: 其他效果: 要素说明: 代码 /** Date: 2023-07-21 15:15:32* LastEditors: ReBeX 420659880qq.com* LastEditTime: 2023-07-27 11:13:17* FilePath: \cesium-tyro-blog\src\utils\Material\EllipsoidFadeMaterialP…...
![](https://www.ngui.cc/images/no-images.jpg)
实战-单例模式和创建生产者相结合
实际中遇到了这样一个问题: The producer group[xxxx] has been created before, specify another instanceName (like producer.setInstanceName) please. 发生的原因是:一个进程内,创建了多个相同topic的producer。 所以问题就转换成了如何…...
![](https://www.ngui.cc/images/no-images.jpg)
[SQL挖掘机] - 窗口函数介绍
介绍: 窗口函数也称为 OLAP 函数。OLAP 是 OnLine AnalyticalProcessing 的简称,意思是对数据库数据进行实时分析处理。窗口函数是一种用于执行聚合计算和排序操作的功能强大的sql函数。它们可以在查询结果集中创建一个窗口(window)…...
![](https://www.ngui.cc/images/no-images.jpg)
原生js实现锚点滚动顶部
简介 使用原生js API实现滚动到指定容器的顶部,API是scrollIntoView 使用 let eldocment.querySelector() 获取dom元素el.scrollIntoView()该元素滚动到其父元素的顶部 高级用法 scrollIntoView(Options)//option可以配置如下 options{behavior:smoot…...
![](https://www.ngui.cc/images/no-images.jpg)
使用mysql接口遇到点问题
game_server加入了dbstorage的代码。dbstorage实现了与mysql的交互:driver_mysql。其中调用了mysql相关的接口。所以game_server需要链接libmysql.lib。 从官网下载了mysql的源码:在用cmake构建mysql工程的时候,遇到了一些问题。 msyql8.0需…...
![](https://img-blog.csdnimg.cn/24fc1941b74e43e5aaf72ee76db82db9.png)
excel绘制折线图或者散点图
一、背景 假如现在通过代码处理了一批数据,想看数据的波动情况,是不是还需要写个pyhon代码,读取文件,绘制曲线,看起来也简单,但是还有更简单的方法,就是直接生成csv文件,csv文件就是…...
![](https://img-blog.csdnimg.cn/0a44ac6c9aac4bf78e1ef4bf7a32a3e5.png)
ChatGPT长文本对话输入方法
ChatGPT PROMPTs Splitter 是一个开源工具,旨在帮助你将大量上下文数据分成更小的块发送到 ChatGPT 的提示,并根据如何处理所有块接收到 ChatGPT(或其他具有字符限制的语言模型)的方法。 推荐:用 NSDT设计器 快速搭建可…...
![](https://www.ngui.cc/images/no-images.jpg)
FFmpeg-swresample的更新
auto convert的创建 在FFmpeg/libavfilter/formats.c中定义了negotiate_video和negotiate_audio,在格式协商,对于video如果需要scale,那么就会自动创建scale作为convert,对于audio,如果需要重采样,则会创建…...
![](https://img-blog.csdnimg.cn/462a915663f142ac9f9078989224f70c.jpeg)
回答网友 修改一个exe
网友说:他有个很多年前的没有源码的exe,在win10上没法用,让俺看一下。 俺看了一下,发现是窗体设计的背景色的问题。这个程序的背景色用的是clInactiveCaptionText。clInactiveCaptionText 在win10之前的系统上是灰色,但…...
![](https://img-blog.csdnimg.cn/93b1cbb28a0941f3a60b79a5c264b461.png)
数据可视化 - 动态柱状图
基础柱状图 通过Bar构建基础柱状图 from pyecharts.charts import Bar from pyecharts.options import LabelOpts # 使用Bar构建基础柱状图 bar Bar() # 添加X轴 bar.add_xaxis(["中国", "美国", "英国"]) # 添加Y轴 # 设置数值标签在右侧 b…...
![](https://img-blog.csdnimg.cn/60abb3c9aadd44d0aff9996f22927d19.png)
【JVM】JVM五大内存区域介绍
目录 一、程序计数器(线程私有) 二、java虚拟机栈(线程私有) 2.1、虚拟机栈 2.2、栈相关测试 2.2.1、栈溢出 三、本地方法栈(线程私有) 四、java堆(线程共享) 五、方法区&…...
![](https://img-blog.csdnimg.cn/e102085b8e41411e9614513d6032cfc8.png)
自动驾驶感知系统--惯性导航定位系统
惯性导航定位 惯性是所有质量体本身的基本属性,所以建立在牛顿定律基础上的惯性导航系统(Inertial Navigation System,INS)(简称惯导系统)不与外界发生任何光电联系,仅靠系统本身就能对车辆进行连续的三维定位和三维定向。卫星导…...
![](https://img-blog.csdnimg.cn/1a1d51311b1045bda3557557873bf6d9.png)
Netty简介
Netty Netty初体验基础概念Reactor模型传统的阻塞IO模型基础Reactor模型多线程Reactor模型 为什么要使用Netty? (NIO的框架,用于解决高并发出现的问题) *BIO:同步且阻塞的IO NIO:同步且非阻塞的IO(不是说线程&#x…...
![](https://www.ngui.cc/images/no-images.jpg)
基于TCP/IP对等模型对计算机网络知识点的整合
目录 前言 应用层 Telnet SSH FTP/TFTP SNMP:简单的网络管理协议 HTTP:超文本传输协议 SMTP:电子邮件传输协议 DNS:域名解析协议 DHCP:动态主机配置协议 NTP:网络时钟协议 传输层 TCP UDP 端…...
![](https://img-blog.csdnimg.cn/30bc732cd41946cb86d2f0082d07933b.gif#pic_center)
【SQL应知应会】表分区(一)• Oracle版
欢迎来到爱书不爱输的程序猿的博客, 本博客致力于知识分享,与更多的人进行学习交流 本文收录于SQL应知应会专栏,本专栏主要用于记录对于数据库的一些学习,有基础也有进阶,有MySQL也有Oracle 分区表 • Oracle版 前言一、分区表1.什么是表分区…...
![](https://www.ngui.cc/images/no-images.jpg)
PostgreSQL 常用空间处理函数
1.OGC标准函数 管理函数: 添加几何字段 AddGeometryColumn(, , , , , ) 删除几何字段 DropGeometryColumn(, , ) 检查数据库几何字段并在geometry_columns中归档 Probe_Geometry_Columns() 给几何对象设置空间参考(在通过一个范围做空间查询时常用&…...
![](https://img-blog.csdnimg.cn/9b876cc901834e69b6847979f68c5457.png)
ubuntu初始化/修改root密码
1.登录ubuntu后,使用sudo passwd root命令,进行root密码的初始化/修改,注:这里需要保证两次输入的密码都是同一个,才可成功 ubuntugt-ubuntu22-04-cmd-v1-0-32gb-100m:~/ocr$ sudo passwd root New password: Retype…...
![](https://img-blog.csdnimg.cn/b91cb4468da34417acce03f08c68f4f1.png)
【Linux后端服务器开发】select多路转接IO服务器
目录 一、高级IO 二、fcntl 三、select函数接口 四、select实现多路转接IO服务器 一、高级IO 在介绍五种IO模型之前,我们先讲解一个钓鱼例子。 有一条大河,河里有很多鱼,分布均匀。张三是一个钓鱼新手,他钓鱼的时候很紧张&a…...
![](https://img-blog.csdnimg.cn/fbb73867ba20493d88f21117cf2f5a1b.png)
支持向量机(iris)
代码: import pandas as pd from sklearn.preprocessing import StandardScaler from sklearn import svm import numpy as np# 定义每一列的属性 colnames [sepal-length, sepal-width, petal-length, petal-width, class] # 读取数据 iris pd.read_csv(data\\i…...
![](https://img-blog.csdnimg.cn/f8460fa6496b489b80c66381595a86aa.png)
24考研数据结构-第二章:线性表
目录 第二章:线性表2.1线性表的定义(逻辑结构)2.2 线性表的基本操作(运算)2.3 线性表的物理/存储结构(确定了才确定数据结构)2.3.1 顺序表的定义2.3.1.1 静态分配2.3.1.2 动态分配2.3.1.3 mallo…...
![](https://www.ngui.cc/images/no-images.jpg)
Mybatis 动态 sql 是做什么的?都有哪些动态 sql?能简述动态 sql 的执行原理不?
OGNL表达式 OGNL,全称为Object-Graph Navigation Language,它是一个功能强大的表达式语言,用来获取和设置Java对象的属性,它旨在提供一个更高的更抽象的层次来对Java对象图进行导航。 OGNL表达式的基本单位是"导航链"&a…...
![](https://www.ngui.cc/images/no-images.jpg)
250_C++_typedef std::function<int(std::vector<int> vtBits)> fnChkSstStt
假设我们需要定义一个函数类型来表示一个能够计算整数向量中所有元素之和的函数。 首先,我们定义一个函数,它的参数是一个 std::vector 类型的整数向量,返回值是 int 类型,表示所有元素之和: int sumVectorElements(std::vector<int> vt) {int sum = 0;for (int n…...
![](https://www.learnfk.com/guide/images/wuya.png)
无涯教程-jQuery - Transfer方法函数
Transfer 效果可以与effect()方法一起使用。这会将元素的轮廓转移到另一个元素。尝试可视化两个元素之间的交互时非常有用。 Transfer - 语法 selector.effect( "transfer", {arguments}, speed ); 这是所有参数的描述- className - 传输元素将收到的可选类名。…...
![](https://img-blog.csdnimg.cn/img_convert/66e275769c838bd850b071e4b5440110.jpeg)
openGauss学习笔记-24 openGauss 简单数据管理-模式匹配操作符
文章目录 openGauss学习笔记-24 openGauss 简单数据管理-模式匹配操作符24.1 LIKE24.2 SIMILAR TO24.3 POSIX正则表达式 openGauss学习笔记-24 openGauss 简单数据管理-模式匹配操作符 数据库提供了三种独立的实现模式匹配的方法:SQL LIKE操作符、SIMILAR TO操作符…...
![](https://img-blog.csdnimg.cn/2ea113b8646f4a1f936efad8f67148b2.png)
JAVASE---数据类型与变量
1. 字面常量 常量即程序运行期间,固定不变的量称为常量,比如:一个礼拜七天,一年12个月等。 public class Demo{ public static void main(String[] args){ System.Out.println("hello world!"); System.Out.println(…...
![](https://img-blog.csdnimg.cn/6d6e651230f849d1bbd09b2edc8e225c.png)
IDEA Groovy 脚本一键生成实体类<mybatisplus>
配置数据库(mysql) 一键生成(右键点击table) 配置自己的groovy脚本 import com.intellij.database.model.DasTable import com.intellij.database.util.Case import com.intellij.database.util.DasUtil import com.intellij.data…...
![](/images/no-images.jpg)
网站怎么添加统计代码/让顾客进店的100条方法
1. 系统权限unlimited tablespace是隐含在dba, resource角色中的一个系统权限. 当用户得到dba或resource的角色时, unlimited tablespace系统权限也隐式受权给用户. 2. 系统权限unlimited tablespace不能被授予role, 可以被授予用户. 3. 系统权限unlimited tablespace不会随着r…...
![](https://img-blog.csdnimg.cn/img_convert/55a2638139d68369d49b3058cd5d88e8.png)
在wordpress上添加播放视频教程/杭州百度公司在哪里
手机型号云服务器 内容精选换一换如果对弹性云服务器执行重启/关机操作,弹性云服务器长时间(大于30分钟)处于“正在重启”/“正在关机”状态时,建议执行强制重启/强制关机操作。登录管理控制台。单击管理控制台左上角的,选择区域和项目。选择…...
![](/images/no-images.jpg)
网站建设企业排名/咸阳seo
protected transient int modCount 0;这个属性是记录这个List被修改的次数。在以下几个内部类和非public类中使用。private class Itr implements Iterator首先先看这个内部类,实现了迭代器接口。int cursor 0;这个变量是游标。int lastRet -1;这个变量代表的是上…...
![](https://img2018.cnblogs.com/blog/814849/201904/814849-20190424135657773-1968718247.png)
如何把地图放到自己做的网站上/百度推广登录地址
转载于:https://www.cnblogs.com/lianghong881018/p/10761990.html...
![](/images/no-images.jpg)
软件开发外包app/seo关键字优化教程
$(#subtabs a).each(function (i, ele) { var href $(ele).attr("href"); if (location.href.indexOf(href) > -1) { $(".tab").removeClass("now-tab"); $(ele).parent().addClass(now-tab); } });...
![](/images/no-images.jpg)
怎么做网站空间/市场调研分析报告
不能用技术的角度去分析事务 SONY的PSP,我们知道大多数都是黑色的。但也有其它颜色,不过价格就要比黑色贵很多。其实从技术的角度来说,东西都是一样的,人家换了一个颜色的壳子,就要比黑色壳子多卖200元左右,…...