开发板:TQ2440 (NandFlash:256M 内存:64M)
u-boot版本:u-boot-2015.04
内核版本:Linux-3.14
作者:彭东林
摘要
这篇博客的目的是简要分析两种spi驱动的实现,一种是利用Samsung的S3C2440自带的硬件SPI控制器,另一种是利用Linux内核已经写好的用GPIO模拟SPI时序,实现一个软件SPI控制器。操作的外设是韦东山的SPI视频教程中提供的OLED模块,同时分享一下在使用逻辑分析仪Saleae16调试SPI时遇到的问题。
相关的内核代码已经上传:git@code.csdn.net:pengdonglin137/linux-3-14-y.git
可以看看代码提交记录。
正文
SPI驱动实现之硬件控制器
一、驱动框架
二、代码
SPI硬件控制器
这里采用的是platform架构,分为device和driver两个部分。
1、platform_device
文件:arch/arm/plat-samsung/devs.c
struct resource s3c_spi0_resource[] = {
2: [0] = DEFINE_RES_MEM(S3C24XX_PA_SPI, SZ_32),
3: [1] = DEFINE_RES_IRQ(IRQ_SPI0),
4: };
5:
int pol)
7: {
8: gpio_set_value(cs, pol);
9: }
10:
struct s3c2410_spi_info s3c_spi_info[] = {
12: {
13: .num_cs = S3C_GPIO_END,
14: .bus_num = 0,
15: .set_cs = s3c24xx_spi_set_cs,
16: }
17: };
18:
struct platform_device s3c_device_spi0 = {
,
21: .id = 0,
22: .num_resources = ARRAY_SIZE(s3c_spi0_resource),
23: .resource = s3c_spi0_resource,
24: .dev = {
25: .dma_mask = &samsung_device_dma_mask,
26: .coherent_dma_mask = DMA_BIT_MASK(32),
void *)s3c_spi_info,
28: }
29: };
第15行是片选函数,它的第二个参数cs来自spi从设备的板级信息,表示这个从设备的片选引脚;
第14行表示spi控制器的编号是0,将来在spi从设备的板级信息中有体现,意思是将来这个spi从设备挂载在编号为0的spi总线下面;
第27行,在linux原生的代码中没有实现platform_data,在调用probe函数的时候会报错;
2、platform_driver
文件:drivers/spi/spi-s3c24xx.c
);
struct platform_driver s3c24xx_spi_driver = {
3: .probe = s3c24xx_spi_probe,
4: .remove = s3c24xx_spi_remove,
5: .driver = {
,
7: .owner = THIS_MODULE,
8: .pm = S3C24XX_SPI_PMOPS,
9: },
10: };
11: module_platform_driver(s3c24xx_spi_driver);
12:
OLED 板级信息
这里调用了spi子系统提供的函数接口。
1、板级信息
文件:arch/arm/mach-s3c24xx/mach-tq2440.c
/* SPI OLED */
struct spi_board_info tq2440_spi_board_info[] __initdata = {
3: {
,
5: .max_speed_hz = 10000000,
6: .bus_num = 0,
7: .mode = SPI_MODE_0,
8: .chip_select = S3C2410_GPG(1),
void *)S3C2410_GPF(3),
10: },
11: };
12:
struct platform_device *tq2440_devices[] __initdata = {
14: ......
15: &s3c_device_spi0,
16: };
17:
void)
19: {
20: ......
21: spi_register_board_info(tq2440_spi_board_info, ARRAY_SIZE(tq2440_spi_board_info));
22: ......
23: }
24:
)
26: ......
27: .init_machine = tq2440_machine_init,
28: ......
29: MACHINE_END
第4行,将来会跟驱动中的name进行匹配;
第5行,表示通信速率,这里设置的是10MHz;
第6行,表示使用的spi总线的编号是0;
第7行,表示使用的spi模式是0,这里要根据oled的芯片手册(SSD1306-Revision 1.1 (Charge Pump).pdf)
第8行,oled使用的片选引脚;
第9行,用于区分命令和数据模式的GPIO资源,这个会在驱动中解析;
第21行,注册spi从设备板级信息;
2、oled驱动
文件:drivers/spi/oled/spi_oled_drv.c
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <sound/core.h>
#include <linux/spi/spi.h>
asm/uaccess.h>
10:
#include <mach/hardware.h>
#include <mach/regs-gpio.h>
13:
#include <linux/gpio.h>
#include <plat/gpio-cfg.h>
16:
/* 构造注册 spi_driver */
18:
int major;
class;
21:
int spi_oled_dc_pin;
struct spi_device *spi_oled_dev;
char *ker_buf;
25:
char val)
27: {
28: gpio_set_value(spi_oled_dc_pin, val);
29: }
30:
char cmd)
32: {
/* command */
34: spi_write(spi_oled_dev, &cmd, 1);
/* */
36: }
37:
char dat)
39: {
/* data */
41: spi_write(spi_oled_dev, &dat, 1);
/* */
43: }
44:
void)
46: {
47: OLEDWriteCmd(0x20);
48: OLEDWriteCmd(0x02);
49: }
50:
int col)
52: {
/* page address */
54:
/* Lower Column Start Address */
/* Lower Higher Start Address */
57: }
58:
59:
void)
61: {
int page, i;
for (page = 0; page < 8; page ++)
64: {
65: OLEDSetPos(page, 0);
for (i = 0; i < 128; i++)
67: OLEDWriteDat(0);
68: }
69: }
70:
int page)
72: {
int i;
74: OLEDSetPos(page, 0);
for (i = 0; i < 128; i++)
76: OLEDWriteDat(0);
77: }
78:
void)
80: {
/* 向OLED发命令以初始化 */
/*display off*/
/*set lower column address*/
/*set higher column address*/
/*set display start line*/
/*set page address*/
/*contract control*/
/*128*/
/*set segment remap*/
/*normal / reverse*/
/*multiplex ratio*/
/*duty = 1/64*/
/*Com scan direction*/
/*set display offset*/
95: OLEDWriteCmd(0x00);
/*set osc division*/
97: OLEDWriteCmd(0x80);
/*set pre-charge period*/
99: OLEDWriteCmd(0x1f);
/*set COM pins*/
101: OLEDWriteCmd(0x12);
/*set vcomh*/
103: OLEDWriteCmd(0x30);
/*set charge pump enable*/
105: OLEDWriteCmd(0x14);
106:
107: OLEDSetPageAddrMode();
108:
109: OLEDClear();
110:
/*display ON*/
112: }
113:
114:
#define OLED_CMD_INIT 0x100001
#define OLED_CMD_CLEAR_ALL 0x100002
#define OLED_CMD_CLEAR_PAGE 0x100003
#define OLED_CMD_SET_POS 0x100004
119:
long arg)
121: {
int page;
int col;
124:
switch (cmd)
126: {
case OLED_CMD_INIT:
128: {
129: OLEDInit();
break;
131: }
case OLED_CMD_CLEAR_ALL:
133: {
134: OLEDClear();
break;
136: }
case OLED_CMD_CLEAR_PAGE:
138: {
139: page = arg;
140: OLEDClearPage(page);
break;
142: }
case OLED_CMD_SET_POS:
144: {
145: page = arg & 0xff;
146: col = (arg >> 8) & 0xff;
147: OLEDSetPos(page, col);
break;
149: }
150: }
return 0;
152: }
153:
struct file *file,
char __user *buf,
156: size_t count, loff_t *ppos)
157: {
int ret;
159:
if (count > 4096)
return -EINVAL;
162: ret = copy_from_user(ker_buf, buf, count);
/* data */
164: spi_write(spi_oled_dev, ker_buf, count);
return 0;
166: }
167:
168:
struct file_operations oled_ops = {
170: .owner = THIS_MODULE,
171: .unlocked_ioctl = oled_ioctl,
172: .write = oled_write,
173: };
174:
struct spi_device *spi)
176: {
int ret;
178:
179: spi_oled_dev = spi;
int)dev_get_platdata(&spi->dev);
181:
);
if (ret < 0)
return ret;
185: gpio_direction_output(spi_oled_dc_pin, 0);
186:
#ifndef CONFIG_TQ2440_USE_SPI_GPIO
);
if (ret < 0)
return ret;
191: gpio_direction_output(spi->chip_select, 1);
#endif
193:
194: ker_buf = kmalloc(4096, GFP_KERNEL);
195:
/* 注册一个 file_operations */
, &oled_ops);
198:
);
200:
/* 为了让mdev根据这些信息来创建设备节点 */
/* /dev/oled */
203:
return 0;
205: }
206:
struct spi_device *spi)
208: {
class, MKDEV(major, 0));
class);
);
212:
213: kfree(ker_buf);
214:
return 0;
216: }
217:
struct spi_driver spi_oled_drv = {
219: .driver = {
,
221: .owner = THIS_MODULE,
222: },
223: .probe = spi_oled_probe,
224: .remove = spi_oled_remove,
225: };
226:
void)
228: {
return spi_register_driver(&spi_oled_drv);
230: }
231:
void)
233: {
234: spi_unregister_driver(&spi_oled_drv);
235: }
236:
237: module_init(spi_oled_init);
238: module_exit(spi_oled_exit);
);
);
);
第187行,如果使用的是gpio模拟的spi的话,这个宏CONFIG_TQ2440_USE_SPI_GPIO会配置,这里我们使用的不是gpio模拟的,所以这个宏没有配置;
第182行,申请gpio,这里使用的函数是devm_gpio_request,它的好处是你不用再考虑gpio资源的释放了,系统会自动帮助你完成,类似的还有devm_kmalloc;
内核配置
System Type --->
SAMSUNG S3C24XX SoCs Support --->
[ ] TQ2440 use spi gpio to communicate with peripherals
Device Drivers --->
[*] SPI support --->
<*> Samsung S3C24XX series SPI
<*> Support TQ2440 OLED (from 100ask.com)
应用
1、oled_test.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
9:
11:
/* oled_test init
* oled_test clear
* oled_test clear <page>
* oled_test <page> <col> <string>
*/
17:
#define OLED_CMD_INIT 0x100001
#define OLED_CMD_CLEAR_ALL 0x100002
#define OLED_CMD_CLEAR_PAGE 0x100003
#define OLED_CMD_SET_POS 0x100004
22:
23:
24:
/* page: 0-7
* col : 0-127
* 字符: 8x16象素
*/
char c)
30: {
int i = 0;
/* 得到字模 */
' '];
34:
/* 发给OLED */
//OLEDSetPos(page, col);
//ioctl(fd, OLED_CMD_CLEAR_PAGE, page);
38: ioctl(fd, OLED_CMD_SET_POS, page | (col << 8));
/* 发出8字节数据 */
//for (i = 0; i < 8; i++)
// OLEDWriteDat(dots[i]);
42: write(fd, &dots[0], 8);
43:
//OLEDSetPos(page+1, col);
//ioctl(fd, OLED_CMD_CLEAR_PAGE, page+1);
46: ioctl(fd, OLED_CMD_SET_POS, (page+1) | (col << 8));
/* 发出8字节数据 */
//for (i = 0; i < 8; i++)
// OLEDWriteDat(dots[i+8]);
50: write(fd, &dots[8], 8);
51: }
52:
53:
54:
/* page: 0-7
* col : 0-127
* 字符: 8x16象素
*/
char *str)
60: {
int i = 0;
62:
63: ioctl(fd, OLED_CMD_CLEAR_PAGE, page);
64: ioctl(fd, OLED_CMD_CLEAR_PAGE, page+1);
while (str[i])
66: {
67: OLEDPutChar(fd, page, col, str[i]);
68: col += 8;
if (col > 127)
70: {
71: col = 0;
72: page += 2;
73: ioctl(fd, OLED_CMD_CLEAR_PAGE, page);
74: ioctl(fd, OLED_CMD_CLEAR_PAGE, page+1);
75: }
76: i++;
77: }
78: }
79:
80:
char *cmd)
82: {
);
, cmd);
, cmd);
, cmd);
, cmd);
);
, cmd);
);
);
92: }
93:
char **argv)
95: {
int do_init = 0;
int do_clear = 0;
int do_show = 0;
int page = -1;
int col;
101:
int fd;
103:
))
105: do_init = 1;
))
107: {
108: do_clear = 1;
109: }
))
111: {
112: do_clear = 1;
113: page = strtoul(argv[2], NULL, 0);
114: }
if (argc == 4)
116: {
117: do_show = 1;
118: page = strtoul(argv[1], NULL, 0);
119: col = strtoul(argv[2], NULL, 0);
120: }
121:
if (!do_init && !do_clear && !do_show)
123: {
124: print_usage(argv[0]);
return -1;
126: }
127:
, O_RDWR);
if (fd < 0)
130: {
);
return -1;
133: }
134:
if (do_init)
136: ioctl(fd, OLED_CMD_INIT);
if (do_clear)
138: {
if (page == -1)
140: ioctl(fd, OLED_CMD_CLEAR_ALL);
else
142: {
if (page < 0 || page > 7)
144: {
);
return -1;
147: }
148: ioctl(fd, OLED_CMD_CLEAR_PAGE, page);
149: }
150: }
if (do_show)
152: {
if (page < 0 || page > 7)
154: {
);
return -1;
157: }
if (col < 0 || col > 127)
159: {
);
return -1;
162: }
163:
164: OLEDPrint(fd, page, col, argv[3]);
165: }
return 0;
167: }
168:
SPI驱动实现之软件控制器
一、驱动框架
从图中可以看出,只替换了两个部分,在硬件上使用几个GPIO,不再使用SPI硬件控制器,所以在驱动上也需要做相应的变更,这部分在kernel中已经支持了。
二、代码
下面我们只列一下不同的部分。
SPI GPIO软件控制器
这里采用的也是platform架构。
1、platform_device
struct spi_gpio_platform_data s3c_spi0_gpio_info = {
2: .num_chipselect = S3C_GPIO_END,
3: .miso = S3C2410_GPE(11),
4: .mosi = S3C2410_GPE(12),
5: .sck = S3C2410_GPE(13),
6: };
7:
struct platform_device s3c_device_spi0_gpio = {
,
10: .id = 0,
11: .dev = {
void *)&s3c_spi0_gpio_info,
13: }
14: };
15:
struct platform_device *tq2440_devices[] __initdata = {
17: ......
18: &s3c_device_spi0_gpio
19: };
20:
void)
22: {
23: ......
24: platform_add_devices(tq2440_devices, ARRAY_SIZE(tq2440_devices));
25: ......
26: }
27:
)
29: ......
30: .init_machine = tq2440_machine_init,
31: ......
32: MACHINE_END
第3/4/5行,表示需要spi软件控制器需要使用的gpio引脚,至少需要MISO、SCK、MOSI;
第10行,表示模拟出的spi软件控制器的编号,也就是spi总线编号;
第9行,将来会跟驱动中的name进行匹配;
2、platform_driver
文件:drivers/spi/spi-gpio.c
2: ......
3:
struct platform_driver spi_gpio_driver = {
5: .driver = {
6: .name = DRIVER_NAME,
7: .owner = THIS_MODULE,
8: .of_match_table = of_match_ptr(spi_gpio_dt_ids),
9: },
10: .probe = spi_gpio_probe,
11: .remove = spi_gpio_remove,
12: };
13: module_platform_driver(spi_gpio_driver);
OLED驱动
下面只列出需要注意的地方。
1、OLED板级信息
/* SPI OLED */
struct spi_board_info tq2440_spi_board_info[] __initdata = {
3: {
,
5: .max_speed_hz = 10000000,
6: .bus_num = 0,
7: .mode = SPI_MODE_0,
8: .chip_select = S3C2410_GPG(1),
void *)S3C2410_GPF(3),
#ifdef CONFIG_TQ2440_USE_SPI_GPIO
void *)S3C2410_GPG(1),
#endif
13: },
14: };
第11行,这个表示片选信号,具体参见drivers/spi/spi-gpio.c的实现;
内核配置
System Type --->
SAMSUNG S3C24XX SoCs Support --->
[*] TQ2440 use spi gpio to communicate with peripherals
Device Drivers --->
[*] SPI support --->
<*> GPIO-based bitbanging SPI Master
<*> Support TQ2440 OLED (from 100ask.com)
测试
编译app
arm-linux-gcc -Wall oled_test.c -o oled_test
操作
1: [root@TQ2440 sky]# ./oled_test init
2: [root@TQ2440 sky]# ./oled_test clear
4: [root@TQ2440 sky]#
结果(使用SPI驱动的两种实现方式的实验现象是一样的,只是驱动的内部实现机理不同)
用Saleae16分析SPI时序
上面我们在设置oled板级信息的时候将spi通信的速率设置为了10MHz,我在抓取spi波形的时候,遇到了问题。
现象如下:
上面的图中,CLOCK时钟有些异常,可以看到只抓到7个波形,并且波形不是很均匀,出现很多类似的波形。刚开始我还以为spi控制器出问题了,后来发现,原来我把采样频率从16M提高到50M以后,全都正常了。
我想就是采用率太低的可能,记得有一个香农采样定理,采样信号的频率至少应该是被采信号的两倍。为了印证这个看法,我又做了下面几个测试。
1、将采样频率设置为25M,通信速率为10M
整个波形都没有问题。
2、将采样频率设置为16M,将通讯速率设置为7M
可以看到,至少抓到的还是8个波形,还算正常。
因此,基本验证了我的看法。
完。