嵌入式技术
SPI 从设备芯片的种类非常广泛,包括用于模拟传感器和编解码器的数字/模拟转换器、内存芯片、USB 控制器或以太网适配器等外设,以及其他类型的芯片。
这样的驱动通常在linux看来是一个协议驱动,比如spi flash,负责和MTD系统打交道;比如触摸传感器,需要和input子系统打交道,再比如spi接口的OLED模块。
这样的设备使用的【接口】在驱动中使用struct spi_deivce表示
struct spi_device {
struct device dev;
struct spi_controller *controller;
struct spi_controller *master; /* compatibility layer */
u32 max_speed_hz;
u8 chip_select;
u8 bits_per_word;
bool rt;
#define SPI_NO_TX BIT(31) /* no transmit wire */
#define SPI_NO_RX BIT(30) /* no receive wire */
/*
* All bits defined above should be covered by SPI_MODE_KERNEL_MASK.
* The SPI_MODE_KERNEL_MASK has the SPI_MODE_USER_MASK counterpart,
* which is defined in 'include/uapi/linux/spi/spi.h'.
* The bits defined here are from bit 31 downwards, while in
* SPI_MODE_USER_MASK are from 0 upwards.
* These bits must not overlap. A static assert check should make sure of that.
* If adding extra bits, make sure to decrease the bit index below as well.
*/
#define SPI_MODE_KERNEL_MASK (~(BIT(30) - 1))
u32 mode;
int irq;
void *controller_state;
void *controller_data;
char modalias[SPI_NAME_SIZE];
const char *driver_override;
int cs_gpio; /* LEGACY: chip select gpio */
struct gpio_desc *cs_gpiod; /* chip select gpio desc */
struct spi_delay word_delay; /* inter-word delay */
/* CS delays */
struct spi_delay cs_setup;
struct spi_delay cs_hold;
struct spi_delay cs_inactive;
/* the statistics */
struct spi_statistics statistics;
/*
* likely need more hooks for more protocol options affecting how
* the controller talks to each chip, like:
* - memory packing (12 bit samples into low bits, others zeroed)
* - priority
* - chipselect delays
* - ...
*/
};
linux内核文档中是这样描述的
A "struct spi_device" encapsulates the controller-side interface between those two types of drivers.
因此,应该表示一个接口而不是一个驱动,当然你说这个接口连接的不就是设备吗?这么理解好像也没错。
SPI 设备驱动使用struct spi_driver表示,提供probe驱动入口,老套路了,比如
static int ssd13306_probe(struct spi_device *spi)
以上是函数原型,留下一个疑问,struct spi_device是作为一个对象传进来的,它是什么时候被构造的呢?
&ecspi2{
cs-gpios = < &gpio1 29 GPIO_ACTIVE_LOW >;//GPIO1_29
num-cs = < 1 >;
pinctrl-names = "default";
pinctrl-0 = < &pinctrl_ecspi2 >;
status = "okay";
oled: ssd13306@0{
compatible = "Justice,ssd13306";//自己写的oled驱动
//compatible = "spidev,spidev";//使用spidev通用驱动
//compatible = "solomon,ssd1306";//使用fbtftf的驱动,oled当成fb
spi-cpol;
spi-cpha;
spi-rx-bus-width = < 0 >;
spi-max-frequency = < 10000000 >;
reset-gpios = < &gpio1 27 GPIO_ACTIVE_LOW >;
dc-gpios = < &gpio1 31 GPIO_ACTIVE_HIGH >;
reg = < 0 >;
};
};
ecspi2是soc的SPI控制器,我们使用这个SPI控制器和从设备oled通信,因此要在这个设备树节点下写一个子节点表示OLED设备。下面是必填项:
pinctrl_ecspi2:oled{//没有MISO因为ssd1306 oled不能读取
fsl,pins = <
MX6UL_PAD_UART4_RX_DATA__GPIO1_IO29 0x10b0 //cs引脚
MX6UL_PAD_UART4_TX_DATA__ECSPI2_SCLK 0x10b1
MX6UL_PAD_UART5_TX_DATA__ECSPI2_MOSI 0x10b1
MX6UL_PAD_UART5_RX_DATA__GPIO1_IO31 0x10b1 //DC引脚,分辨数据还是命令
MX6UL_PAD_UART3_RTS_B__GPIO1_IO27 0x10b1 //res复位引脚
>;
};
其余都不是必要属性,但是如果驱动异常,需要进一步调试是不是缺了某些属性。
下面列举了从设备设备树节点可选属性:
属性 | 描述 | |||
---|---|---|---|---|
spi-cpha | spi->mode | = SPI_CPHA | CPHA=1 | |
spi-cpol | spi->mode | = SPI_CPOL | CPOL=1 | |
spi-3wire | spi->mode | = SPI_3WIRE | 使用三线SPI | |
spi-lsb-first | spi->mode | = SPI_LSB_FIRST | 一般spi是MSB,指定后LSB | |
spi-cs-high | spi->mode | = SPI_CS_HIGH | 一般片选CS是低有效,指定后高有效 | |
spi-tx-bus-width | spi->mode | = SPI_NO_TX | 发送方向为0,可能只是读 | |
= SPI_TX_DUAL | DUAL SPI 双线半双工 | |||
= SPI_TX_QUAD | QUAD SPI 四线半双工 | |||
= SPI_TX_OCTAL | OCTAL SPI 八线半双工 | |||
spi-rx-bus-width | spi->mode | = SPI_NO_RX | 接受方向为0,可能只是发送 | |
= SPI_RX_DUAL | DUAL SPI 双线半双工 | |||
= SPI_RX_QUAD | QUAD SPI 四线半双工 | |||
= SPI_RX_OCTAL | OCTAL SPI 八线半双工 | |||
reg | spi->chip_select | 表示spi设备在第几个片选 | ||
spi-max-frequency | spi->max_speed_hz | 这个spi设备使用的spi传输速率,单位Hz |
说说为什么这么设置:
查看ssd13306手册,SPI接口的OLED使用的时钟周期最大是100ns,也就是频率为10M的SPI时钟,因此设置spi-max-frequency = <10000000>;
时钟极性是空闲时为高电平,因此spi-cpol = 1,填上spi-cpol;
数据在第二个边沿锁定,因此spi-cpal = 1,填上spi-cpal;
编写成一个字符设备驱动,提供接口供上层调用。本驱动会不断完善,加入各种新知识运用进来。说说要点:
需要注册字符设备,创建类,创建设备节点
oled_dev- >major = register_chrdev(0, "ssd13306", &ssd13306_fops);
...
oled_dev- >oled_class = class_create(THIS_MODULE, "ssd13306");
...
device_create(oled_dev- >oled_class,NULL, MKDEV(oled_dev- >major, 0), NULL, "ssd13306");
gpiod是较新的gpio描述符,OLED使用到cs、dc、reset三个gpio。
其中dc引脚和SPI协议无关,只和OLED这个模块相关,用来区分发送的是命令还是显示数据。
cs引脚不需要我们自己管理,实际上架构已经为我们获取了。
oled_dev- >dc_gpio = gpiod_get(&spi- >dev, "dc", GPIOD_OUT_LOW);
//初始化dc引脚
dc_gpio_init(oled_dev- >dc_gpio);
oled_dev- >reset_gpio = gpiod_get(&spi- >dev, "reset", GPIOD_OUT_HIGH);
需要发送命令初始化oled,使用spi_write这个SPI架构提供的API可以以同步的方式发送SPI数据,经过源码研究,其实无所谓同步异步了,现架构都是使用异步的,都使用工作者线程来完成spi的传输管理。
static void ssd13306_write_cmd(struct ssd13306_oled *ssd13306,unsigned char cmd)
{
int ret = 0;
dc_gpio_set_value(g_oled- >dc_gpio,0);
ret = spi_write(ssd13306- >ssd13306, &cmd, 1);
if(ret)
dev_err(&ssd13306- >ssd13306- >dev,"err spi write cmd(%d)",ret);
}
//spi 发送函数的原型
spi_write(struct spi_device *spi, const void *buf, size_t len)
dc_gpio_set_value(g_oled->dc_gpio,0)
dc gpio拉低,表示接下来发送的都是命令。
硬件初始化只针对ssd13306,其他OLED模块另外寻找初始化序列。
static void ssd13306_hw_init(struct ssd13306_oled *ssd13306)
{
oled_reset(ssd13306);
ssd13306_write_cmd(ssd13306,0xAE);//关闭oled显示
ssd13306_write_cmd(ssd13306,0xd5);//设置时钟分频因子
ssd13306_write_cmd(ssd13306,80);//[3:0]分频因子,[7:4]震荡频率
ssd13306_write_cmd(ssd13306,0xa8);//设置驱动路数
ssd13306_write_cmd(ssd13306,0x3f);
ssd13306_write_cmd(ssd13306,0xd3);//设置显示偏移
ssd13306_write_cmd(ssd13306,0x00);//设置显示偏移
ssd13306_write_cmd(ssd13306,0x40);//设置显示开始行
ssd13306_write_cmd(ssd13306,0x8d);//设置电荷泵
ssd13306_write_cmd(ssd13306,0x14);//bit2开启或关闭
ssd13306_write_cmd(ssd13306,0x20);//设置寻址模式
ssd13306_write_cmd(ssd13306,0x2);//0x0列地址模式;0x1行地址模式;0x2页地址模式
ssd13306_write_cmd(ssd13306,0xa1);//左右镜像
ssd13306_write_cmd(ssd13306,0xc8);//上下镜像
ssd13306_write_cmd(ssd13306,0xda);//设置com硬件引脚配置
ssd13306_write_cmd(ssd13306,0x12);
ssd13306_write_cmd(ssd13306,0x81);//亮度设置
ssd13306_write_cmd(ssd13306,0xff);
ssd13306_write_cmd(ssd13306,0xd9);//设置预充电周期
ssd13306_write_cmd(ssd13306,0xf1);
ssd13306_write_cmd(ssd13306,0xdb);//设置电压倍率
ssd13306_write_cmd(ssd13306,0x30);//
ssd13306_write_cmd(ssd13306,0xa4);//全局显示开启bit0 :0关闭,1开启
ssd13306_write_cmd(ssd13306,0xa6);//设置显示方式,bit0 :0正常模式,1反相模式
ssd13306_write_cmd(ssd13306,0xaf);//开启oled显示
}
static void oled_clear(unsigned char filldata)
{
int page;
int col;
unsigned char * data;
data = kmalloc(1, GFP_KERNEL);
*data = filldata;
for(page=0;page< 8;page++)
{
ssd13306_write_cmd (g_oled,0xb0+page); //设置页地址(0~7)
ssd13306_write_cmd (g_oled,0x0); //设置显示位置列低地址
ssd13306_write_cmd (g_oled,0x10); //设置显示位置列高地址
for(col=0;col< 128;col++)ssd13306_write_datas(g_oled,data,1);
}
kfree(data);
}