SAMD21 のドライバを書いてみる

ASF (メーカー提供のライブラリ)なしでやってます.

I2S (断念)

master clock を入力もしくは内部の GCLK を利用し SCK (BCLK), FS (LRCK) を出すのはそんなに難しくありません. そこから data を出すのにつまってしまい、適切な解決方法が不明で断念となりました.

この確認はとりあえずソフトから I2S->DATA[x].reg = xx; とするのですが I2S->CLKCTRL[x] への I2S_CLKCTRL_SLOTSIZE_32 でのみ SD (DAC.DAT) が反応することまではわかりました. SLOTSIZE は 16 で使いたかったのですが...

よくわからないのは I2S->DATA[x].reg へ書き込む際の型も 16bit 単位でかけるのかもよくわからなく SLOTSIZE_32 で 32bit 単位で I2S->DATA[x].reg をかけば動くというものでした.

ここからうまく動けば DMA controller に RAM から I2S->DATA[x].reg を定期的に書いてもらうつもりでしたがうまくいかなそうだし、MCU から直接 I2S を出すのはここだけで試して今考えている今後の設計ではつかわないつもりでしたのでここでやめました.

WavPack の tiny decoder の出す decoded data が int32_t で bit15:0 のみが有効な配列のため I2S->DATA[x].reg に書く型は 16 bit で address increment を 4 にするつもりでやってたので無理そうです.

SERCOM.SPI.Master (使えた)

関数を抜粋します.

#define DECLARE_PORT(name) volatile SercomSpi *name = &SERCOM4->SPI
static inline void spi_write(uint8_t data)
{
	DECLARE_PORT(s);
	s->DATA.bit.DATA = data;
	while(s->INTFLAG.bit.TXC == 0);
}

この関数の前に s->CTRLB.bit.RXEN = 0; が必要です. この RXEN は書き換えはいつでもできて同期もいりません. 逆にいうとほかの設定向けレジスタは s->CTRLB.bit.ENABLE = 0; にした上で各種同期がいります.

TX の DMA (polling, 完了割り込みなし)です.

static void dma_write(const uint8_t *src, int src_increment, int length)
{
	uint16_t flag = DMAC_BTCTRL_STEPSIZE_X1 | DMAC_BTCTRL_BEATSIZE_BYTE;
	if(src_increment){
		flag |= DMAC_BTCTRL_SRCINC;
	}
	DECLARE_PORT(s);
	dma_descripter_set(DMACH_SDC_SPI_TX, src, (void *) &s->DATA, length, flag);
	dma_channel_enable_polling(DMACH_SDC_SPI_TX, SERCOM4_DMAC_ID_TX);
	while(s->INTFLAG.bit.TXC == 0);
}

DMACH_xxx は私が定義した DMA channel number のための enum です.

src_increment の切り替えは結構大切です. 意図的に CLOCK を動かして初期化する場面、memset のように一定データで埋めたい場面、RX のための CLOCK を動かす場面など用途が結構あります.
また dma controller における s->DATA の書き込みは SERCOM への同期が含まれていないので DMA controller としては最後のデータを書いた時点で完了となります. よって s->INTFLAG.bit.TXC の同期が必要です.

static inline uint8_t spi_read(void)
{
	DECLARE_PORT(s);
	s->DATA.bit.DATA = 0xff;
	while(s->INTFLAG.bit.RXC == 0);
	return s->DATA.bit.DATA;
}

この関数の前に s->CTRLB.bit.RXEN = 1 が必要です. CLOCK の動作は s->DATA への書き込みによって発生します.

RX の DMA (polling) で、これが曲者でした.

static void dma_read(uint8_t *dest, int length)
{
	const uint16_t flag = DMAC_BTCTRL_STEPSIZE_X1 | DMAC_BTCTRL_BEATSIZE_BYTE | DMAC_BTCTRL_DSTINC | DMAC_BTCTRL_STEPSEL;
	DECLARE_PORT(s);
	dma_descripter_set(DMACH_SDC_SPI_RX, (const void *) &s->DATA, dest, length, flag);
	switch_rx();
	dma_channel_enable(DMACH_SDC_SPI_RX, SERCOM4_DMAC_ID_RX);
	
	const uint8_t ff = 0xff;
	dma_write(&ff, 0, length);
	dma_channel_disable(DMACH_SDC_SPI_RX);
}

SERCOMx_DMAC_ID_RX だけの DMA をいれても CLOCK が動かないので、TX も並列動作させます. TX の書き込みは increment なしで ff というローカル変数使ってますけど、割り込みでこの関数を抜ける場合(ffが無効になる場合)はいつでも無効にならない static 変数を利用する必要があります.
DMA channel number は TX < RX として, 転送量も同じにして、 RX を polling なしで動かしてから、 TX を polling します. また dma_write() のように同期がいります. (RXC の同期はいらない気がして抜きました)

dma_write() は 4 byte とか 8 byte でも使ってもいいんですが、 dma_read() は動作の波形をみると初期化に時間がかかるので 10 byte 以上に使ったほうがいいです. さらに転送量が多くなると当然割り込み待ちにもなるんですがまだそこまで試す段階にはなってません.

dma_read は forum の書き込みを参考に対処させてもらいました. DMA controller の使い方はよくわかってなかったのですが、channel 並列動作の場合は、channel を切り替えて順番にやることもここで理解しました.

*1

*1:dma_descripter_set(), dma_channel_enable_polling() は初期化をまとめたもので割愛します.