TLC5615 SPI DAC KMOD 开发记录

TLC5615算是能找到的最简单的一块SPI设备了. 顺带KGDB调试配置记录.

 

模块基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/of.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/list.h>
#include <linux/mutex.h>
#include <linux/spi/spi.h>
#include <linux/module.h>

MODULE_LICENSE("GPL");

OF匹配表

1
2
3
4
5
6
7
8
9
10
static const struct of_device_id tlc5615_ids[] = {
{ .compatible = "ti,tlc5615" },
{},
};

static const struct spi_device_id tlc5615_id[] = {
{"tlc5615", 0},
{},
};
MODULE_DEVICE_TABLE(spi, tlc5615_id);

SPI驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
static int tlc5615_probe(struct spi_device *spi);
static int tlc5615_remove(struct spi_device *spi);
static struct spi_driver tlc5615_driver = {
.driver = {
.owner = THIS_MODULE,
.name = "tlc5615",
.of_match_table = of_match_ptr(tlc5615_ids),
},
.probe = tlc5615_probe,
.remove = tlc5615_remove,
.id_table = tlc5615_id
};
module_spi_driver(tlc5615_driver);

字符设备

1
2
3
4
5
6
7
8
9
10
11
12
#define TLC_DEVICE_NAME "tlc5615"
static uint tlc_major = 0;
static struct class *tlc_class = NULL;

int tlc5615_open(struct inode *inode, struct file *filp);
ssize_t tlc5615_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_ops);

struct file_operations tlc5615_fops = {
.owner = THIS_MODULE,
.write = tlc5615_write,
.open = tlc5615_open,
};

数据结构体

1
2
3
4
5
6
struct tlc5615 {
struct mutex lock; // avoid spi implict
struct spi_device *spi; // used when sending data.
struct cdev cdev; // per-device data
struct device* dev;
};

实体

probe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
struct spi_message msg = {0, };
struct spi_transfer xfer = {0, };
struct tlc5615 *dac;
uint16_t tx_data = 0x0000; // initial DAC value
int err;
int devno;

/* spi device settings */
spi->mode = SPI_MODE_0;
spi->max_speed_hz = 10000000;
spi->bits_per_word = 8;
err = spi_setup(spi);
if(err<0) return err;

/* transfer contents */
xfer.tx_nbits = 1;
xfer.tx_buf = &tx_data;
xfer.bits_per_word = 8;
xfer.len = 2;
xfer.speed_hz = 10000000;
spi_message_init(&msg);
spi_message_add_tail(&xfer, &msg);
if((err = spi_sync(spi, &msg))<0) return -EFAULT;

/* device data setup */
dac = devm_kzalloc(&spi->dev, sizeof(struct tlc5615), GFP_KERNEL);
if(dac == NULL)return -ENOMEM;
dac->spi = spi;

/* chrdev setup */
err = alloc_chrdev_region(&devno, 0, 1, TLC_DEVICE_NAME);
if(err<0)return err;
tlc_major = MAJOR(devno);

tlc_class = class_create(THIS_MODULE, TLC_DEVICE_NAME);
if(IS_ERR(tlc_class)) return PTR_ERR(tlc_class);

cdev_init(&dac->cdev, &tlc5615_fops);
dac->cdev.owner = THIS_MODULE;
err = cdev_add(&dac->cdev, devno, 1);
if(err){
/* destroy assigned class */
class_destroy(tlc_class);
return err;
}

dac->dev = device_create(tlc_class, NULL, devno, NULL, "tlc5615");
if(IS_ERR(dac->dev)){
cdev_del(&dac->cdev);
class_destroy(tlc_class);
return PTR_ERR(dac->dev);
}
/* set device data to driver. */
spi_set_drvdata(spi, dac);
return 0;

remove

probe申请了一个device, 一个cdev, 一个class和一个region.
设备数据中还有一个互斥锁.

1
2
3
4
5
6
7
struct tlc5615 *dac = spi_get_drvdata(spi);
device_del(dac->dev);
cdev_del(&dac->cdev);
class_destroy(tlc_class);
unregister_chrdev_region(MKDEV(tlc_major, 0), 1);
mutex_destroy(&dac->lock);
return 0;

open

没什么要做的, 就是把设备数据放到file里面.

1
2
filp->private_data = container_of(inode->i_cdev, struct tlc5615, cdev);
return 0;

write

写数据.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct tlc5615 *dac = filp->private_data;
struct spi_transfer xfer = {0, };
struct spi_message msg = {0, };
uint16_t* buffer;
int i;
/* 10bits DAC, padding to 16bits */
if(count % 2)return -EINVAL;
/* alloc space for operating */
buffer = kmalloc(count, GFP_KERNEL);
if(buffer == NULL)return -ENOMEM;
/* move data to kernel space */
if(copy_from_user(buffer, buf, count)!=0){
kfree(buffer);
return -EFAULT;
}
/* fit chip's data format */
for(i = 0; i < (count << 1); i++){
buffer[i] = __builtin_bswap16((buffer[i] & 0x3ff) << 2);
}
/* transfer */
xfer.tx_buf = buffer;
xfer.len = count;
xfer.bits_per_word = 8;
xfer.speed_hz = 10000000;
spi_message_init(&msg);
spi_message_add_tail(&xfer, &msg);
i = spi_sync(dac->spi, &msg);

/* cleanup and return. */
kfree(buffer);
if(i < 0)return i;
return count;

Debug

算是一点额外的内容.
因为最开始没有初始化spi_transfer结构体, 里面的delay完全就是随机数. 在传输检查的时候delay的mode找不到返了一个-EINVAL回来. 没上调试器之前完全不知道是哪里返回来的. (上了调试器也有够麻烦
V3s的JTAG和SDC0只能二选一(显然用SD卡启动的没得选), 只能使用KDB和KGDB进行调试.

准备带kgdb/kdb的内核

到Linux内核进行menuconfig, 搜索kdb以后打开就行了. 另外一个FRAME_POINTER看起来ARM是没法用的. 另外还有一个调试符号也需要设为y. 重新编译以后把zImage扔回第一分区.

设备树

调试的时候WIFI不稳定模块时不时往串口打印一堆奇怪东西, 没办法再开了一个串口. V3s还好把有一个UART2没人用, 抄UART0的配置再在别名里面加一个serial1=&uart2, 把kgdboc调到ttyS1,115200做调试口. UART0上次扔到i2c1去了这回还需要禁用i2c1并重新启用uart0. alias也要加回去.

重编译dtbs扔回第一分区.

启动参数

修改boot.cmd在setenv那里加一个kgdboc=ttyS1,115200, 用mkimage重新打包boot.scr扔回第一分区.(到这里mmcblk0p1已经没有一个文件是没换过的了.

调试准备

pacman -S gdb-multiarch, 这个包是archlinuxcn里面的, 需要添加archlinuxcn的源.

gdb-multiarch vmlinux按照提示准备好.gdbinit, 还可以在.gdbinit里面加一条set serial baud 115200.

使用Windows的话还是好好用COM转TCP, 自己找了一个好用的软件commfront Serial-TCP, 非商用免费.

进入调试

开机以后直接echo g > /proc/sysrq-trigger, 宿主机gdb vmlinux以后载入远程调试目标target remote /dev/ttyUSB0, 用telnet的把/dev/ttyUSB0换成对应ip:port就行.

这种方法还有一个好处就是可以在linux虚拟机下面执行调试不需要把COM分配到虚拟机.

远程调试载入完了执行lx-symbols自动载入模块符号. 不使用lx-symbols要一个个从/proc/modules里面把base address找出来, 完了还要扣ko里面的段地址一个个添加到gdb有够麻烦的.

开始调试

抓probe函数需要在载入模块前添加断点, lx-symbols以后允许预注册断点, gdb命令b tlc5615_probe可以在符号载入后产生断点. 调write函数可以在载入以后再添加断点. 放置断点后c继续运行.

载入完了等进入断点就跟平常使用gdb一摸一样了. 至于怎么触发断点也蛮麻烦的. 只有一个串口可以与目标及通信的目前没发现什么好办法. 像我这种不仅开俩串口还有WIFI和EMAC的倒是可以继续走串口或者ssh载入模块. 注册一个IO口挂shell做while(1)循环检测也可以. (要是没有其他的通信方法还不给jtag那不调了. 老老实实pr_info.

实际上kgdb还是有点不稳定的. 能用jtag还是jtag好. 调试过程中跑飞,, 段错误, 取不到值经常有出现. 前一句还被optimized out后一句就取到值了. 前一句i=-1152921504606后一句i<0就为假. 传个东西突然就没了, 只能断电再来. 顺便这个时候中个中断串口打点奇怪的东西调试直接结束. (pr_info走串口的也算.

调半天找到问题是没有初始化结构体变量导致delay这一条检查过不去. 初始化以后就没问题了. (为什么我看人家都不带初始化的淦

时序测试

python没办法给一个文件写16位数据, 所以用nodejs的writeUInt16LE函数好一点.

放while(1)里面给劲传. 逻辑分析仪开400M采样采. (实际10MHz用10M采样率完全变形, 用25M采有一段不对, 气到开400M采(实际50M够了

调到大致和目标一样了以后拆掉逻辑分析仪上DAC.

0x00f0这个值的时候, 输入Vref=3.3V, 输出电压0.77V, 刚好发光二极管有点亮. 万用表测到0.763V. (实际上不可以这么玩. 输入Vin=5V, Vref_max=Vin-2V=3V, 没炸掉算我运气好. 找不到电阻还不想用2.5V LDO的情况下就随便拿3.3V来玩玩.