TLC5615 SPI DAC ALSA 开发记录

想玩玩Alsa但是这个垃圾片子又不带I2S接口有一个I2S的PCM模块用不上. 干脆前段时间玩过的DAC拿出来当Alsa输出玩玩(x 下次玩玩DAI, 看起来更方便点.

 

模块基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

#include <linux/spi/spi.h>
#include <linux/slab.h>
#include <linux/of.h>
#include <linux/device.h>
#include <linux/workqueue.h>
#include <linux/delay.h>
#include <linux/uaccess.h>

#include <sound/pcm.h>
#include <sound/control.h>
#include <sound/pcm_params.h>
#include <sound/core.h>
#include <sound/initval.h> // SNDRV_DEFAULT_*
#include <sound/asound.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_snd" },
{},
};

static const struct spi_device_id tlc5615_id[] = {
{"tlc5615_snd", 0},
{},
};
MODULE_DEVICE_TABLE(spi, tlc5615_id);
1
2
3
4
5
6
7
&spi0 {
tlc5615: tlc5615@0 {
compatible = "ti,tlc5615_snd";
reg = <0>;
spi-max-freqency = <20000000>;
};
};

SPI驱动表

1
2
3
4
5
6
7
8
9
10
11
static struct spi_driver tlc5615_driver = {
.driver = {
.owner = THIS_MODULE,
.name = "tlc5615_snd",
.of_match_table = of_match_ptr(tlc5615_ids),
},
.probe = tlc5615_snd_probe,
.remove = tlc5615_snd_remove,
.id_table = tlc5615_id
};
module_spi_driver(tlc5615_driver);

设备结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct tlc5615_snd {
struct snd_card *card;
struct snd_pcm *pcm;
struct spi_device *spi;
//struct hrtimer timer; // top half
//struct tasklet_struct tasks; // bottom half
//ktime_t delay;
uint16_t* buffer; // cpu accessible address
dma_addr_t dma_buffer; // to free resource
uint32_t position; // current buffer position
uint8_t timer_running_flag; // response PCM_TRIGGER_STOP
size_t buffer_size; // avoid out of bounds
uint16_t volume; //
uint16_t data_to_write; // not used.
struct work_struct work; // spi_write thread.
};

声卡驱动声明

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/* sound/initval.h 
#define SNDRV_DEFAULT_IDX { [0 ... (SNDRV_CARDS-1)] = -1 }
#define SNDRV_DEFAULT_STR { [0 ... (SNDRV_CARDS-1)] = NULL }
#define SNDRV_DEFAULT_ENABLE { 1, [1 ... (SNDRV_CARDS-1)] = 0 }
#define SNDRV_DEFAULT_ENABLE_PNP { [0 ... (SNDRV_CARDS-1)] = 1 }
*/

static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
static bool enable[SNDRV_CARDS] = SNDRV_DEFAULT_ENABLE_PNP;

static int tlc5615_snd_new_pcm(struct tlc5615_snd* chip);

static int tlc5615_snd_volume_info(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_info *uinfo);

static int tlc5615_snd_volume_get(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol);

static int tlc5615_snd_volume_put(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol);

static struct snd_kcontrol_new tlc5615_control = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Master Playback Volume",
.index = 0,
.info = tlc5615_snd_volume_info,
.get = tlc5615_snd_volume_get,
.put = tlc5615_snd_volume_put,
.private_value = 0xffff, // not used.
};

static int snd_tlc5615_probe(struct spi_device *spi){
/* function snd_tlc5615_probe */
static int dev;
struct snd_card *card;
struct tlc5615_snd *chip;
int err;

/* can skip. */
if(dev > SNDRV_CARDS)
return -ENODEV;
if(!enable[dev]) {
dev ++;
return -ENOENT;
}

/* spi setup */
spi->max_speed_hz = 20000000;
spi->mode = SPI_MODE_0;
spi->bits_per_word = 8;
err = spi_setup(spi);
if(err < 0) {
pr_err("spi_setup failed: %d\n", err);
return err;
}

/* to create a card instance */
err = snd_card_new(&spi->dev, index[dev], id[dev], THIS_MODULE, sizeof(struct tlc5615_snd), &card);
if(err < 0) {
pr_err("snd_card_new failed, %d\n", err);
return err;
}

/* init driver data */
chip = card->private_data;
chip->card = card;
chip->spi = spi;
strcpy(card->driver, "tlc5615");
strcpy(card->shortname, "tlc5615 fake sound");
strcpy(card->longname, "ti,tlc5615 10bit dac pcm device");
strcpy(card->mixername, "tlc5615 mixer");
INIT_WORK(&chip->work, tlc5615_transfer);

/* It would be better to create a constructor for pcm, namely */
err = tlc5615_snd_new_pcm(chip);
if(err < 0){
pr_info("snd new pcm failed, %d\n", err);
}

/*A component can be created via snd_device_new function. */
err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &tlc5615_ops);
if(err < 0){
pr_info("snd device new failed%d\n", err);
return err;
}

/* To create a control, there are two functions to be called. */
err = snd_ctl_add(card, snd_ctl_new1(&tlc5615_control, chip));
if(err < 0){
struct snd_kcontrol *kctl;
kctl = snd_ctl_find_numid(card, 0);
pr_info("ctl add failed.\n");
if(kctl)
snd_ctl_remove(card, kctl);
}

/* Register the card instance. */
err = snd_card_register(card);
if(err < 0){
pr_info("snd card register failed, %d\n", err);
goto failed;
}

spi_set_drvdata(spi, card);
dev ++;
return 0;
failed:
snd_card_free(card);
return err;
}

note: { [start ... end]=0 } 索引初始化, 是GNU扩展.

PCM设备声明

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
static int tlc5615_snd_playback_open(struct snd_pcm_substream * substream);
static int tlc5615_snd_playback_close(struct snd_pcm_substream * substream);
static int tlc5615_snd_pcm_trigger(struct snd_pcm_substream * substream, int cmd);
static int tlc5615_snd_pcm_prepare(struct snd_pcm_substream *substream);
static snd_pcm_uframes_t tlc5615_snd_pcm_pointer(struct snd_pcm_substream *substream);

static struct snd_pcm_ops tlc5615_playback_ops = {
.open = tlc5615_snd_playback_open,
.close = tlc5615_snd_playback_close,
.trigger = tlc5615_snd_pcm_trigger,
.prepare = tlc5615_snd_pcm_prepare,
.pointer = tlc5615_snd_pcm_pointer,
.ioctl = snd_pcm_lib_ioctl,
};

static int tlc5615_snd_new_pcm(struct tlc5615_snd* chip)
{
struct snd_pcm *pcm;
int err;

/* allocate pcm instance */
err = snd_pcm_new(chip->card, "tlc5615_snd",
0, // index from zero
1, // 1 playback substream
0, // 0 capture substream
&pcm);
if(err < 0) {
pr_info("snd_pcm_new failed, %d", err);
return err;
}
pcm->private_data = chip;
strcpy(pcm->name, "tlc5615_snd");
chip->pcm = pcm;

/* After the pcm is created, you need to set operators for each pcm stream. */
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK,
&tlc5615_playback_ops);

/* you probably will want to pre-allocate the
buffer and set up the managed allocation mode */
snd_pcm_set_managed_buffer_all(pcm,
SNDRV_DMA_TYPE_CONTINUOUS,
&chip->spi->dev,
64*1024,
64*1024);
// pcm->private_free will be called when pcm destruct.
return 0;
}

设备运作方式

一般使用DMA, 设定好触发源. 可惜V3s在主线Linux的SPI驱动里面并没有添加DMA支持.
snd_pcm_set_managed_buffer_all提供一个dma_area作为CPU可访问的逻辑地址. 利用这个字段可以不使用DMA进行数据传输.
可以选择hrtimer作为触发源, 普通timer精度可能不够.
若既不使用DMA又选择使用中断就需要选择合适的bottom-half.
显然实时性好又可以进入睡眠的bottom-half并不存在.
spi_async并没有被V3s的SPI驱动支持.
干脆就再开个线程给死传, 加一个变量检测传输状态.

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
// write to device.
static void tlc5615_write(struct spi_device* spi, uint16_t data)
{
struct tlc5615_snd *chip = spi_get_drvdata(spi);
data = __builtin_bswap16(((data >> 6) << 2);
spi_write(spi, &data, 2);
}
static void tlc5615_transfer(struct work_struct *work)
{
struct tlc5615_snd *chip = container_of(work, struct tlc5615_snd, work);
uint16_t buffer_d[16];
msleep(20); // wait for some data.
while(chip->timer_running_flag){
int i = 16; // 16 bit per block.
uint16_t* buffer;
if(chip->position > chip->buffer_size >> 1){
chip->position = 0;
}
buffer = &chip->buffer[chip->position];
while(i--){
register int32_t tmp2 = *(int16_t*)&buffer[i];
register uint16_t tmp;
tmp2 *= chip->volume; // volume control
tmp2 /= 100; // max value 100
tmp = *(uint16_t*)&tmp2;// get low 16bit.
tmp += 32768; // bias
tmp >>= 6; // 10 bit value, 128 volume
tmp <<= 2; // 2 bit dummy value
tmp = __builtin_bswap16(tmp); // to BE.
buffer_d[i] = tmp & 0xffff; // write back.
}
spi_write(chip->spi, buffer_d, 16);
chip->position += 16;
}
}

PCM驱动构建

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
static struct snd_pcm_ops tlc5615_playback_ops = {
.open = tlc5615_snd_playback_open,
.close = tlc5615_snd_playback_close,
.trigger = tlc5615_snd_pcm_trigger,
.prepare = tlc5615_snd_pcm_prepare,
.pointer = tlc5615_snd_pcm_pointer,
.ioctl = snd_pcm_lib_ioctl,
};
static struct snd_pcm_hardware tlc5615_snd_playback_hw = {
.info = (SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER ),
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 1,
.channels_max = 1,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
/* open callback */
static int tlc5615_snd_playback_open(struct snd_pcm_substream * substream)
{
struct tlc5615_snd *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
/* in the open callback, you can modify the copied descriptor runtime->hw as you need */
runtime->hw = tlc5615_snd_playback_hw;
tlc5615_write(chip->spi, 0);
return 0;
}
/* close callback */
static int tlc5615_snd_playback_close(struct snd_pcm_substream * substream)
{
struct tlc5615_snd *chip = snd_pcm_substream_chip(substream);
tlc5615_write(chip->spi, 0);
return 0;
}
/* trigger callback */
static int tlc5615_snd_pcm_trigger(struct snd_pcm_substream * substream,
int cmd)
{
struct tlc5615_snd *chip = snd_pcm_substream_chip(substream);
switch(cmd) {
case SNDRV_PCM_TRIGGER_START:
chip->timer_running_flag = 1;
/* start tlc5615_transfer */
schedule_work(&chip->work);
break;
case SNDRV_PCM_TRIGGER_STOP:
chip->timer_running_flag = 0;
/* wait all transfer done. */
cancel_work_sync(&chip->work);
break;
default:
return -EINVAL;
}
return 0;
}
/* hw_params callback - nothing to configure */
/* prepare callback */
static int tlc5615_snd_pcm_prepare(struct snd_pcm_substream *substream)
{
struct tlc5615_snd *chip = snd_pcm_substream_chip(substream);
struct spi_device *spi = chip->spi;
struct snd_pcm_runtime *runtime = substream->runtime;
/* check format */
if(runtime->format != SNDRV_PCM_FORMAT_S16_LE)
return -EINVAL;
/* set transfer props */
chip->buffer = (uint16_t*)runtime->dma_area;
chip->buffer_size = runtime->buffer_size << 1;
chip->position = 0;
/* 16 bit, set speed same as frame rate. */
spi->max_speed_hz = runtime->rate * 16;
spi_setup(spi);
return 0;
}
/* pointer callback */
static snd_pcm_uframes_t tlc5615_snd_pcm_pointer(struct snd_pcm_substream *substream)
{
struct tlc5615_snd *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
snd_pcm_uframes_t pos;
pos = bytes_to_frames(runtime, chip->position >> 1);
if (pos >= runtime->buffer_size)
pos -= runtime->buffer_size;
return pos;
}

控制接口构建

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
static struct snd_kcontrol_new tlc5615_control = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Master Playback Volume",
.index = 0,
.info = tlc5615_snd_volume_info,
.get = tlc5615_snd_volume_get,
.put = tlc5615_snd_volume_put,
.private_value = 0xffff,
};
/* CONTROL INTERFACE */
static int tlc5615_snd_volume_info(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo)
{
uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
uinfo->count = 1;
uinfo->value.integer.min = 0;
uinfo->value.integer.max = 100;
return 0;
}

static int tlc5615_snd_volume_get(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct tlc5615_snd *chip = snd_kcontrol_chip(kcontrol);
ucontrol->value.integer.value[0] = chip->volume;
return 0;
}

static int tlc5615_snd_volume_put(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct tlc5615_snd *chip = snd_kcontrol_chip(kcontrol);
chip->volume = ucontrol->value.integer.value[0] % 100;
return 1;
}

Debug

实际没什么调的. 主要还是没有DMA. 缺少控制接口没有办法在/dev/snd下生成pcmD1C0p, 没有办法直接播放.
不太敢直接写寄存器, 万一哪个DMA已经被分配走了我再拿用估计又得白费力气. 不知道可不可以先向系统申请一个DMA再iomap写寄存器.
缺少DMA, 用逻辑分析仪看每次启动spi需要将近50us的时间, 传一次22us的时间, 相当于传16次空两次. buffer用尽以后最长一次重新装填用了5ms.


没有加高通滤波器的时候全是杂音, 加了高通滤波器好多了. 虽然噪声还是蛮大的至少能听清音乐的声音了.
倒是碰见一些奇怪的问题. ffmpeg更新以后依赖的libicu还是74版本的, 需要重新下74版本的libicu交叉编译以后放回去. 重新编译libicu需要在主机编译一份以后再进行交叉编译, 交叉编译时要指定主机编译的目录.


Ref: writing-an-alsa-driver.rst,
sound/spi/at73c213.c