はじめに
Raspberry Pi アドベントカレンダー 2022の記事です。
https://qiita.com/advent-calendar/2022/raspberry-pi
https://adventar.org/calendars/7440
Raspberry Pi Zero Wを使って遊んだ記録をシェアします。
記事中、Raspberry Pi Zero WをRPIZWって書きます。
この記事の目標というかやりたかったこと
- Raspberry Pi Zero Wで
- OSなしでHDMI経由でHDMI Audioもレジスタ叩けばもちろん再生できても良いはず
- Raspberry Pi Zero W(RPIZW)でOS無しの状態でbootloader起動
- VC4 HDMI Audioレジスタを調べて叩いててさらにwavファイルをべた置きで再生する
- という工作をして遊ぼうというもの(ソフト書くだけ)
- DREQ信号でDMA転送使ってHDMIのfifoにデータを書き込んで音をならします。CPUはDMA kickのみ(実験なので)
- 今年の3, 4月位に調べていた事柄のセーブポイントみたいなものです
調べ方とか前提とか
- 私はHDMI Audioのこと何も知らない人でひとまずraspbianのdriverを参考にモノマネをして音が出せればいいという工作までできたら満足になります
- ちゃんと音が鳴りました。demoがあります。
- VC4の公式のHDMIのレジスタに関するドキュメントがlinuxのsourceくらいしかなく、端的に言うと読みながら作ったもので不完全なものという認識。間違ってたら指摘ください。
- 割り切ってraspbianでALSAでPCM再生しているときのレジスタを調べてある程度実現しました。
- 必要なところ以外は叩きません(start.elfが設定している箇所と思われる値はそのまま)
- -640x480x32のMAILBOX_FRAMEBUFFERコマンドを実行した状態からstartします
- 後で出てきますが、vc4_hdmi.cのraspberry piのissueは頻繁にたてられていて良く更新される場所の模様でこのhdmi audio周り調べ始めたのが2022/3月くらいなので書いた情報が古いかも知れません
機材とか
-
Raspberry Pi Zero W
https://www.raspberrypi.com/products/raspberry-pi-zero-w/
無い人はRaspberry Pi2でもOK(なはず -
USB シリアル変換機 (uartとteratermがつながればなんでもいいです)
適当なmicroUSB用電源 -
microUSB to USB変換アダプタ(XBOX360コントローラとつなぐときに必要)
HDMI to USB変換機 (PCからはカメラデバイスとして見える) -
ワシはこれをつかいました : https://www.amazon.co.jp/dp/B07X5VJQ3D
OBS Studio
※なぜOBSなのかは前の記事参照(要はキャプチャとかそういうの取りたいし人によっては楽)
- HDMIの画像信号と音声信号のsplitter
デバッグ用。HDMI to USB変換機でも音が出るに出るんですが私が買ったのはモノラルになるのでLRちゃんと音出てるか確認できなかったので新たに買ったものです。
Raspberry PiのHDMI周りのIPについて
- raspberry piの公式PDFはHDMIレジスタの仕様などについては全く触れられていません。
しかたがないのでLinuxの仕様やalsa再生時にどんなレジスタの設定になっているのかをざっと追いました。VC4にimplされているのはfalcom HDMI moduleというもの。以下から淡々と調べていきます。
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_hdmi.c#L9
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_regs.h#L539
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_hdmi.c#L2136
aplayだとIEC958_SUBFRAME_LEで再生されてました(巻末にverboseも記載)
- 公式issueだと以下がとても参考になりました
https://github.com/raspberrypi/linux/issues/4654
https://prodigytechno.com/hdmi-protocol/
https://en.wikipedia.org/wiki/AES3
おおざっぱに動かすためのレジスタ設定を行う
調べると以下のようです。
1.HDMI_MAI_CTL : enablebit=1, chnum=1, chalign=1
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_regs.h#L753
2.HDMI_MAI_FMT
audio formatは以下から参考に設定
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_regs.h#L562
samplerateは以下から参考に設定
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_regs.h#L573
3.HDMI_MAI_THR : DMA転送で使うDREQシグナル発行の閾値(単位不明でかつ頻繁にissueで変更されている
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_regs.h#L777
それぞれどんな挙動なのかまずわからないのでdemoではalsa動いているときの値をそのまま後述のいれてます
4.HDMI_MAI_CONFIG : frameのformat周りを設定。
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_regs.h#L544
5.HDMI_MAI_CHANNEL_MAP
https://github.com/raspberrypi/linux/blob/b63605b13ef77f0720ce5bba6864e76f07429c97/drivers/gpu/drm/vc4/vc4_hdmi.c#L2538
6.HDMI_AUDIO_PACKET_CONFIG : このレジスタの設定が本当にわからないんですがheaderの定義からみると、IEC953のframe周りの処理をhw側でしてくれる処理の設定などという認識。有識者求む...
https://github.com/raspberrypi/linux/blob/b63605b13ef77f0720ce5bba6864e76f07429c97/drivers/gpu/drm/vc4/vc4_hdmi.c#L2539
7.HDMI_MAI_SMP, HDMI_CRP_CFG, HDMI_CTS_0, HDMI_CTS_1 :
spec不明...HDMI側のクロックとsamplerateから値作って放り込んでるのでraspbianで実行しているときの値をそのまま定数でいれちゃいました
https://github.com/raspberrypi/linux/blob/aeaa2460db088fb2c97ae56dec6d7d0058c68294/drivers/gpu/drm/vc4/vc4_hdmi.c#L1827
8.HDMI_MAI_DATAにしかるべきIEC958の32bit formatでデータを詰めると音が出る...もよう
https://github.com/raspberrypi/linux/blob/7f1f585bd1bd24656fc915e72cbda5c36b07221e/drivers/gpu/drm/vc4/vc4_hdmi_regs.h#L85
ALSAでは8.はDMA転送でHDMI_MAI_DATAにblock転送を行いながら処理をする模様です。
で、成果物
demo (youtube)は以下です。
がっつり音が鳴っています。これはaudio capture + HDMI to USBなのでモノラルですがステレオwav再生できます。
Sample_HDMI_DMA_Audio_03で使っているHDMI AudioのHDMI_MAI_DATAへのCPUを使わない非同期DMA転送
- Raspberry PiのDMA転送は良い記事があるので割愛します。TransterControlBlockのformatに従ってCPUでデータを構築、DMAChannelにblock群を設定してcontrol status bitを1にすると勝手に転送してくれるものです。
- 再掲ですがbcm2835-peripherals.pdfをみます
上記PDFの4.2.1.3 Peripheral DREQ Signalsに概要があるのですが、周辺機器へCPUとは非同期にDMA転送を行うときにDMA転送時に周辺機器からのイベントをとらえて勝手にデータ転送してくれる仕組みがあるらしくそれを使います。
以下の手順で行ってみました
1.PCMデータをIEC958の形式にCPU側で頑張って変換
https://github.com/kumaashi/RaspberryPI/blob/master/RPIZEROW/Sample_HDMI_DMA_Audio_03/main.c#L254
※多分wavheaderまで変換しているが特に気にせず...厳密にはheaderをより分けないといけないけど。
2.バッファのReadアドレスをRPIDMAのレジスタの設定
https://github.com/kumaashi/RaspberryPI/blob/master/RPIZEROW/Sample_HDMI_DMA_Audio_03/main.c#L271
3.HDMI_MAI_DATAのWriteアドレスをincrementなし4byteで設定
4.TIBlockのDREQを有効にして、PERMAPに11(HDMI)を設定する
https://github.com/kumaashi/RaspberryPI/blob/master/RPIZEROW/Sample_HDMI_DMA_Audio_03/main.c#L276
5.HDMI_MAI_THR設定, DREQLOW=0x8, DREQHIGH=0x6, PANICLOW=0x8, PANICHIGH=0x8 : (raspbianだと定数で0x08080608)
https://github.com/kumaashi/RaspberryPI/blob/master/RPIZEROW/Sample_HDMI_DMA_Audio_03/main.c#L176
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_regs.h#L777
6.DMA転送をCPU側で開始してあとは放置(DMAが勝手にやってくれる)
https://github.com/kumaashi/RaspberryPI/blob/master/RPIZEROW/Sample_HDMI_DMA_Audio_03/main.c#L286
原理的にはリングバッファを使えば、armv6側で定期的にシグナルを受けてデータを変換、バッファに書き込んで、stream転送ができそうです。
まだそこまで手をつけられていません。
ディスプレイという観点からの互換性
今のところこんな感じ。
https://github.com/kumaashi/RaspberryPI/blob/c1cc3c3e600efcf74e6ebe7134ef13d40bd15ff4/RPIZEROW/Sample_HDMI_Audio_Workbench/readme.md
もうちょっというとなんか海外からお問い合わせがあって俺のディスプレイで動く動かないとかそういうのがありました。
(githubちゃんとみられてるんですね...)
いずれにせよ多分今の書き方だと「ディスプレイ側の実力」で音が鳴ってるかもしれない程度でなんかまだまだちゃんと書かないといけない。
まとめ
- まとまっていないですがなんか音は鳴らすことができています。ただ控えめに言ってまだ出来損ないな気がします
- baremetalな環境でやるにはちょっと資料が足らなさすぎるのでコスト高い気がします
- IEC958のformatを作るところがちょっと面倒で、自分でmixする場合は少しやりざまを考えないといけないです。
- 多分搭載されているシングルコアのarmv6では変換しながらほかに何かするというのは気軽にはまだできない気がします。
- HDMI Audio系の記事もしなにか参考になるものがありましたら教えていただけるとうれしいです
その他 : raspbian上でALSA動かしている最中にHDMI系、VC4_HDMI_MAI系のregister dumpするやつ
それぞれ完全なsource以下に貼っておきます。ALSAでwavファイル再生しながらひたすら実コードとレジスタの値が一致するかをチェックしていました
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdint.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <limits.h>
#include <signal.h>
#include <unistd.h>
#include <sys/uio.h>
#include <sys/mman.h>
#define BASE_ADDR 0x20902000 //HDMI
#define HDMI_BASE ((uint32_t)start_addr)
#define HDMI_CORE_REV ((volatile uint32_t *)(HDMI_BASE + 0x0000))
#define HDMI_SW_RESET_CONTROL ((volatile uint32_t *)(HDMI_BASE + 0x0004))
#define HDMI_HOTPLUG_INT ((volatile uint32_t *)(HDMI_BASE + 0x0008))
#define HDMI_HOTPLUG ((volatile uint32_t *)(HDMI_BASE + 0x000c))
#define HDMI_FIFO_CTL ((volatile uint32_t *)(HDMI_BASE + 0x005c))
#define HDMI_MAI_CHANNEL_MAP ((volatile uint32_t *)(HDMI_BASE + 0x0090))
#define HDMI_MAI_CONFIG ((volatile uint32_t *)(HDMI_BASE + 0x0094))
#define HDMI_MAI_FORMAT ((volatile uint32_t *)(HDMI_BASE + 0x0098))
#define HDMI_AUDIO_PACKET_CONFIG ((volatile uint32_t *)(HDMI_BASE + 0x009c))
#define HDMI_RAM_PACKET_CONFIG ((volatile uint32_t *)(HDMI_BASE + 0x00a0))
#define HDMI_RAM_PACKET_STATUS ((volatile uint32_t *)(HDMI_BASE + 0x00a4))
#define HDMI_CRP_CFG ((volatile uint32_t *)(HDMI_BASE + 0x00a8))
#define HDMI_CTS_0 ((volatile uint32_t *)(HDMI_BASE + 0x00ac))
#define HDMI_CTS_1 ((volatile uint32_t *)(HDMI_BASE + 0x00b0))
#define HDMI_SCHEDULER_CONTROL ((volatile uint32_t *)(HDMI_BASE + 0x00c0))
#define HDMI_HORZA ((volatile uint32_t *)(HDMI_BASE + 0x00c4))
#define HDMI_HORZB ((volatile uint32_t *)(HDMI_BASE + 0x00c8))
#define HDMI_VERTA0 ((volatile uint32_t *)(HDMI_BASE + 0x00cc))
#define HDMI_VERTB0 ((volatile uint32_t *)(HDMI_BASE + 0x00d0))
#define HDMI_VERTA1 ((volatile uint32_t *)(HDMI_BASE + 0x00d4))
#define HDMI_VERTB1 ((volatile uint32_t *)(HDMI_BASE + 0x00d8))
#define HDMI_CEC_CNTRL_1 ((volatile uint32_t *)(HDMI_BASE + 0x00e8))
#define HDMI_CEC_CNTRL_2 ((volatile uint32_t *)(HDMI_BASE + 0x00ec))
#define HDMI_CEC_CNTRL_3 ((volatile uint32_t *)(HDMI_BASE + 0x00f0))
#define HDMI_CEC_CNTRL_4 ((volatile uint32_t *)(HDMI_BASE + 0x00f4))
#define HDMI_CEC_CNTRL_5 ((volatile uint32_t *)(HDMI_BASE + 0x00f8))
#define HDMI_CEC_TX_DATA_1 ((volatile uint32_t *)(HDMI_BASE + 0x00fc))
#define HDMI_CEC_TX_DATA_2 ((volatile uint32_t *)(HDMI_BASE + 0x0100))
#define HDMI_CEC_TX_DATA_3 ((volatile uint32_t *)(HDMI_BASE + 0x0104))
#define HDMI_CEC_TX_DATA_4 ((volatile uint32_t *)(HDMI_BASE + 0x0108))
#define HDMI_CEC_RX_DATA_1 ((volatile uint32_t *)(HDMI_BASE + 0x010c))
#define HDMI_CEC_RX_DATA_2 ((volatile uint32_t *)(HDMI_BASE + 0x0110))
#define HDMI_CEC_RX_DATA_3 ((volatile uint32_t *)(HDMI_BASE + 0x0114))
#define HDMI_CEC_RX_DATA_4 ((volatile uint32_t *)(HDMI_BASE + 0x0118))
#define HDMI_TX_PHY_RESET_CTL ((volatile uint32_t *)(HDMI_BASE + 0x02c0))
#define HDMI_TX_PHY_CTL_0 ((volatile uint32_t *)(HDMI_BASE + 0x02c4))
#define HDMI_CEC_CPU_STATUS ((volatile uint32_t *)(HDMI_BASE + 0x0340))
#define HDMI_CEC_CPU_SET ((volatile uint32_t *)(HDMI_BASE + 0x0344))
#define HDMI_CEC_CPU_CLEAR ((volatile uint32_t *)(HDMI_BASE + 0x0348))
#define HDMI_CEC_CPU_MASK_STATUS ((volatile uint32_t *)(HDMI_BASE + 0x034c))
#define HDMI_CEC_CPU_MASK_SET ((volatile uint32_t *)(HDMI_BASE + 0x0350))
#define HDMI_CEC_CPU_MASK_CLEAR ((volatile uint32_t *)(HDMI_BASE + 0x0354))
#define HDMI_RAM_PACKET_START ((volatile uint32_t *)(HDMI_BASE + 0x0400))
#define HDMI_RAM_PACKET(ch, x) ((volatile uint32_t *)(HDMI_BASE + (0x24 * ch) + 0x0400 + (4 * (x))))
int main() {
int mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
void *map_base = mmap(NULL,
4096,
PROT_READ | PROT_WRITE,
MAP_SHARED,
mem_fd,
BASE_ADDR ); // phys_addr should be page-aligned.
uint32_t *start_addr = (uint32_t *)map_base;
uint32_t *virt_addr = (uint32_t *)map_base;
start_addr = (uint32_t *)map_base;
printf("HDMI_CORE_REV = 0x%08X\n",*HDMI_CORE_REV );
printf("HDMI_SW_RESET_CONTROL = 0x%08X\n",*HDMI_SW_RESET_CONTROL );
printf("HDMI_HOTPLUG_INT = 0x%08X\n",*HDMI_HOTPLUG_INT );
printf("HDMI_HOTPLUG = 0x%08X\n",*HDMI_HOTPLUG );
printf("HDMI_FIFO_CTL = 0x%08X\n",*HDMI_FIFO_CTL );
printf("HDMI_MAI_CHANNEL_MAP = 0x%08X\n",*HDMI_MAI_CHANNEL_MAP );
printf("HDMI_MAI_CONFIG = 0x%08X\n",*HDMI_MAI_CONFIG );
printf("HDMI_MAI_FORMAT = 0x%08X\n",*HDMI_MAI_FORMAT );
printf("HDMI_AUDIO_PACKET_CONFIG= 0x%08X\n",*HDMI_AUDIO_PACKET_CONFIG );
printf("HDMI_RAM_PACKET_CONFIG = 0x%08X\n",*HDMI_RAM_PACKET_CONFIG );
printf("HDMI_RAM_PACKET_STATUS = 0x%08X\n",*HDMI_RAM_PACKET_STATUS );
printf("HDMI_CRP_CFG = 0x%08X\n",*HDMI_CRP_CFG );
printf("HDMI_CTS_0 = 0x%08X\n",*HDMI_CTS_0 );
printf("HDMI_CTS_1 = 0x%08X\n",*HDMI_CTS_1 );
printf("HDMI_SCHEDULER_CONTROL = 0x%08X\n",*HDMI_SCHEDULER_CONTROL );
printf("HDMI_HORZA = 0x%08X\n",*HDMI_HORZA );
printf("HDMI_HORZB = 0x%08X\n",*HDMI_HORZB );
printf("HDMI_VERTA0 = 0x%08X\n",*HDMI_VERTA0 );
printf("HDMI_VERTB0 = 0x%08X\n",*HDMI_VERTB0 );
printf("HDMI_VERTA1 = 0x%08X\n",*HDMI_VERTA1 );
printf("HDMI_VERTB1 = 0x%08X\n",*HDMI_VERTB1 );
printf("HDMI_CEC_CNTRL_1 = 0x%08X\n",*HDMI_CEC_CNTRL_1 );
printf("HDMI_CEC_CNTRL_2 = 0x%08X\n",*HDMI_CEC_CNTRL_2 );
printf("HDMI_CEC_CNTRL_3 = 0x%08X\n",*HDMI_CEC_CNTRL_3 );
printf("HDMI_CEC_CNTRL_4 = 0x%08X\n",*HDMI_CEC_CNTRL_4 );
printf("HDMI_CEC_CNTRL_5 = 0x%08X\n",*HDMI_CEC_CNTRL_5 );
printf("HDMI_CEC_TX_DATA_1 = 0x%08X\n",*HDMI_CEC_TX_DATA_1 );
printf("HDMI_CEC_TX_DATA_2 = 0x%08X\n",*HDMI_CEC_TX_DATA_2 );
printf("HDMI_CEC_TX_DATA_3 = 0x%08X\n",*HDMI_CEC_TX_DATA_3 );
printf("HDMI_CEC_TX_DATA_4 = 0x%08X\n",*HDMI_CEC_TX_DATA_4 );
printf("HDMI_CEC_RX_DATA_1 = 0x%08X\n",*HDMI_CEC_RX_DATA_1 );
printf("HDMI_CEC_RX_DATA_2 = 0x%08X\n",*HDMI_CEC_RX_DATA_2 );
printf("HDMI_CEC_RX_DATA_3 = 0x%08X\n",*HDMI_CEC_RX_DATA_3 );
printf("HDMI_CEC_RX_DATA_4 = 0x%08X\n",*HDMI_CEC_RX_DATA_4 );
printf("HDMI_TX_PHY_RESET_CTL = 0x%08X\n",*HDMI_TX_PHY_RESET_CTL );
printf("HDMI_TX_PHY_CTL_0 = 0x%08X\n",*HDMI_TX_PHY_CTL_0 );
printf("HDMI_CEC_CPU_STATUS = 0x%08X\n",*HDMI_CEC_CPU_STATUS );
printf("HDMI_CEC_CPU_SET = 0x%08X\n",*HDMI_CEC_CPU_SET );
printf("HDMI_CEC_CPU_CLEAR = 0x%08X\n",*HDMI_CEC_CPU_CLEAR );
printf("HDMI_CEC_CPU_MASK_STATUS= 0x%08X\n",*HDMI_CEC_CPU_MASK_STATUS );
printf("HDMI_CEC_CPU_MASK_SET = 0x%08X\n",*HDMI_CEC_CPU_MASK_SET );
printf("HDMI_CEC_CPU_MASK_CLEAR = 0x%08X\n",*HDMI_CEC_CPU_MASK_CLEAR );
printf("HDMI_RAM_PACKET_START = 0x%08X\n",*HDMI_RAM_PACKET_START );
*HDMI_RAM_PACKET_CONFIG = 0x1000c;
/*
while(1) {
for(int i = 0 ; i < 9; i++) {
//*HDMI_RAM_PACKET(4, i) = 0;
//*HDMI_RAM_PACKET(5, i) = 0;
}
}
*/
for(int ch = 0 ; ch <= 8; ch++) {
for(int i = 0 ; i < 9; i++) {
printf("HDMI_RAM_PACKET(%d, %d)=0x%08X\n", ch, i, *HDMI_RAM_PACKET(ch, i));
}
}
/*
for(int ch = 4 ; ch <= 8; ch++) {
for(int i = 0 ; i < 9; i++) {
*HDMI_RAM_PACKET(ch, i) = rand();
}
}
*/
printf("\n");
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdint.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <limits.h>
#include <signal.h>
#include <unistd.h>
#include <sys/uio.h>
#include <sys/mman.h>
//#define BASE_ADDR 0x20902000 //HDMI
#define BASE_ADDR 0x20808000 //HD
#define HD_BASE ((uint32_t)start_addr)
#define HDMI_M_CTL ((volatile uint32_t *)(HD_BASE + 0x000c))
#define HDMI_MAI_CTL ((volatile uint32_t *)(HD_BASE + 0x0014))
#define HDMI_MAI_THR ((volatile uint32_t *)(HD_BASE + 0x0018))
#define HDMI_MAI_FMT ((volatile uint32_t *)(HD_BASE + 0x001c))
#define HDMI_MAI_DATA ((volatile uint32_t *)(HD_BASE + 0x0020))
#define HDMI_MAI_DATA1 ((volatile uint32_t *)(HD_BASE + 0x0024))
#define HDMI_MAI_SMP ((volatile uint32_t *)(HD_BASE + 0x002c))
#define HDMI_VID_CTL ((volatile uint32_t *)(HD_BASE + 0x0038))
#define HDMI_CSC_CTL ((volatile uint32_t *)(HD_BASE + 0x0040))
#define HDMI_CSC_12_11 ((volatile uint32_t *)(HD_BASE + 0x0044))
#define HDMI_CSC_14_13 ((volatile uint32_t *)(HD_BASE + 0x0048))
#define HDMI_CSC_22_21 ((volatile uint32_t *)(HD_BASE + 0x004c))
#define HDMI_CSC_24_23 ((volatile uint32_t *)(HD_BASE + 0x0050))
#define HDMI_CSC_32_31 ((volatile uint32_t *)(HD_BASE + 0x0054))
#define HDMI_CSC_34_33 ((volatile uint32_t *)(HD_BASE + 0x0058))
#define HDMI_FRAME_COUNT ((volatile uint32_t *)(HD_BASE + 0x0068))
uint32_t arandom() {
static uint32_t a = 1;
static uint32_t b = 12038971;
static uint32_t c = 48590871;
a += b;
b += c;
c += a;
return (a >> 16);
}
int main() {
int mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
void *map_base = mmap(NULL,
4096,
PROT_READ | PROT_WRITE,
MAP_SHARED,
mem_fd,
BASE_ADDR); // phys_addr should be page-aligned.
uint32_t *start_addr = (uint32_t *)map_base;
uint32_t *virt_addr = (uint32_t *)map_base;
printf("HDMI_MAI_CTL =0x%08X\n", * HDMI_MAI_CTL );
printf("HDMI_MAI_THR =0x%08X\n", * HDMI_MAI_THR );
printf("HDMI_MAI_FMT =0x%08X\n", * HDMI_MAI_FMT );
printf("HDMI_MAI_DATA =0x%08X\n", * HDMI_MAI_DATA );
printf("HDMI_MAI_DATA1 =0x%08X\n", * HDMI_MAI_DATA1 );
printf("HDMI_MAI_SMP =0x%08X\n", * HDMI_MAI_SMP );
printf("HDMI_VID_CTL =0x%08X\n", * HDMI_VID_CTL );
printf("HDMI_CSC_CTL =0x%08X\n", * HDMI_CSC_CTL );
printf("HDMI_CSC_12_11 =0x%08X\n", * HDMI_CSC_12_11 );
printf("HDMI_CSC_14_13 =0x%08X\n", * HDMI_CSC_14_13 );
printf("HDMI_CSC_22_21 =0x%08X\n", * HDMI_CSC_22_21 );
printf("HDMI_CSC_24_23 =0x%08X\n", * HDMI_CSC_24_23 );
printf("HDMI_CSC_32_31 =0x%08X\n", * HDMI_CSC_32_31 );
printf("HDMI_CSC_34_33 =0x%08X\n", * HDMI_CSC_34_33 );
printf("HDMI_FRAME_COUNT =0x%08X\n", * HDMI_FRAME_COUNT );
printf("\n");
return 0;
}
こいつらをraspbian上でgcc系でmakeして実行すればひとまずdumpできます。
raspbian上でALSA再生中にレジスタを雑に乗っ取ってwavファイルを再生するサンプル(aplay時のログ付き)
/*
aplay: pcm_write:2058: write error: Interrupted system call
root@raspberrypi:~# aplay --channels=100 /boot/ipa.wav --verbose
Playing WAVE '/boot/ipa.wav' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
Plug PCM: Soft volume PCM
Control: PCM Playback Volume
min_dB: -51
max_dB: 0
resolution: 256
Its setup is:
stream : PLAYBACK
access : RW_INTERLEAVED
format : S16_LE
subformat : STD
channels : 2
rate : 44100
exact rate : 44100 (44100/1)
msbits : 16
buffer_size : 22052
period_size : 5513
period_time : 125011
tstamp_mode : NONE
tstamp_type : MONOTONIC
period_step : 1
avail_min : 5513
period_event : 0
start_threshold : 22052
stop_threshold : 22052
silence_threshold: 0
silence_size : 0
boundary : 1445199872
Slave: IEC958 subframe conversion PCM (IEC958_SUBFRAME_LE)
Its setup is:
stream : PLAYBACK
access : MMAP_INTERLEAVED
format : S16_LE
subformat : STD
channels : 2
rate : 44100
exact rate : 44100 (44100/1)
msbits : 16
buffer_size : 22052
period_size : 5513
period_time : 125011
tstamp_mode : NONE
tstamp_type : MONOTONIC
period_step : 1
avail_min : 5513
period_event : 0
start_threshold : 22052
stop_threshold : 22052
silence_threshold: 0
silence_size : 0
boundary : 1445199872
Slave: Hooks PCM
Its setup is:
stream : PLAYBACK
access : MMAP_INTERLEAVED
format : IEC958_SUBFRAME_LE
subformat : STD
channels : 2
rate : 44100
exact rate : 44100 (44100/1)
msbits : 24
buffer_size : 22052
period_size : 5513
period_time : 125011
tstamp_mode : NONE
tstamp_type : MONOTONIC
period_step : 1
avail_min : 5513
period_event : 0
start_threshold : 22052
stop_threshold : 22052
silence_threshold: 0
silence_size : 0
boundary : 1445199872
Slave: Hardware PCM card 0 'vc4-hdmi' device 0 subdevice 0
Its setup is:
stream : PLAYBACK
access : MMAP_INTERLEAVED
format : IEC958_SUBFRAME_LE
subformat : STD
channels : 2
rate : 44100
exact rate : 44100 (44100/1)
msbits : 24
buffer_size : 22052
period_size : 5513
period_time : 125011
tstamp_mode : NONE
tstamp_type : MONOTONIC
period_step : 1
avail_min : 5513
period_event : 0
start_threshold : 22052
stop_threshold : 22052
silence_threshold: 0
silence_size : 0
boundary : 1445199872
appl_ptr : 0
hw_ptr : 0
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdint.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <limits.h>
#include <signal.h>
#include <unistd.h>
#include <sys/uio.h>
#include <sys/mman.h>
uint32_t arandom() {
static uint32_t a = 1;
static uint32_t b = 12038971;
static uint32_t c = 48590871;
a += b;
b += c;
c += a;
return (a >> 16);
}
static unsigned int iec958_parity(unsigned int data)
{
unsigned int parity;
int bit;
data >>= 4; /* start from bit 4 */
parity = 0;
for (bit = 4; bit <= 30; bit++) {
if (data & 1)
parity++;
data >>= 1;
}
return (parity & 1);
}
void print_regs(uint32_t *start_addr) {
printf("HDMI_MAI_CTL =0x%08X\n", * HDMI_MAI_CTL );
printf("HDMI_MAI_THR =0x%08X\n", * HDMI_MAI_THR );
printf("HDMI_MAI_FMT =0x%08X\n", * HDMI_MAI_FMT );
printf("HDMI_MAI_DATA =0x%08X\n", * HDMI_MAI_DATA );
printf("HDMI_MAI_DATA1 =0x%08X\n", * HDMI_MAI_DATA1 );
printf("HDMI_MAI_SMP =0x%08X\n", * HDMI_MAI_SMP );
printf("HDMI_VID_CTL =0x%08X\n", * HDMI_VID_CTL );
printf("HDMI_CSC_CTL =0x%08X\n", * HDMI_CSC_CTL );
printf("HDMI_CSC_12_11 =0x%08X\n", * HDMI_CSC_12_11 );
printf("HDMI_CSC_14_13 =0x%08X\n", * HDMI_CSC_14_13 );
printf("HDMI_CSC_22_21 =0x%08X\n", * HDMI_CSC_22_21 );
printf("HDMI_CSC_24_23 =0x%08X\n", * HDMI_CSC_24_23 );
printf("HDMI_CSC_32_31 =0x%08X\n", * HDMI_CSC_32_31 );
printf("HDMI_CSC_34_33 =0x%08X\n", * HDMI_CSC_34_33 );
printf("HDMI_FRAME_COUNT =0x%08X\n", * HDMI_FRAME_COUNT );
}
int main() {
int mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
void *map_base = mmap(NULL,
4096,
PROT_READ | PROT_WRITE,
MAP_SHARED,
mem_fd,
BASE_ADDR); // phys_addr should be page-aligned.
uint32_t *start_addr = (uint32_t *)map_base;
uint32_t *virt_addr = (uint32_t *)map_base;
print_regs(start_addr);
/*
*HDMI_CSC_CTL = 0;
*HDMI_CSC_12_11 = 0;
*HDMI_CSC_14_13 = 0;
*HDMI_CSC_22_21 = 0;
*HDMI_CSC_24_23 = 0;
*HDMI_CSC_32_31 = 0;
*HDMI_CSC_34_33 = 0;
*/
//*HDMI_MAI_CTL |= ~1;
//*HDMI_MAI_FMT = 0x20800;
*HDMI_MAI_FMT = 0x20800;
//*HDMI_VID_CTL = 0xC0080000;
*HDMI_MAI_SMP = 0x0DCD21F3;
//*HDMI_MAI_THR = 0x10101010;
*HDMI_MAI_THR = 0x02020202;
*HDMI_MAI_CTL |= 29;
//file read
printf("start file read\n");
std::vector<uint32_t> vdata;
FILE *fp = fopen("/boot/ipa.wav", "rb");
if(fp) {
while(1) {
uint32_t data = 0;
int ret = fread(&data, 1, sizeof(uint16_t), fp);
if(ret <= 0)
break;
data <<= 16;
vdata.push_back(data);
}
}
fclose(fp);
printf("end file read\n");
int global_count = 0;
int counter = 0;
int ch = 0;
uint32_t *pdata = vdata.data();
while(1) {
global_count++;
uint32_t data = *pdata++;
counter %= 192;
ch %= 2;
unsigned int byte = counter >> 3;
unsigned int mask = 1 << (counter - (byte << 3));
data >>= 4;
data &= ~0xF;
//data |= 0x40000000;
data |= 0x10000000;
//todo status
if(iec958_parity(data))
data |= 0x80000000;
/* 0x08, 0x02, 0x04 /* Z, X, Y */
if(counter == 0) {
data |= 0x08; //B
} else if(ch == 1) {
data |= 0x04; //W
} else {
data |= 0x02; //M
}
//todo
int loop_count = 0;
int busy_th = 0x100;
if((global_count % 16) == 0) {
volatile int wait_cnt = 0x100;
while(wait_cnt--);
}
while(*HDMI_MAI_CTL & (1 << 11)) {
loop_count++;
if(loop_count > busy_th) {
loop_count = 0;
//printf("BUSY global_count=%d, %08X\n", global_count, *HDMI_MAI_CTL);
*HDMI_MAI_CTL |= (1<< 2 );
*HDMI_MAI_CTL |= (1<< 1 );
}
}
*HDMI_MAI_DATA = data;
counter++;
ch++;
}
printf("\n");
return 0;
}
-
最初はCPUで実験をして挙動を確認していました。
-
HDMI_MAI_CTLのbusybitがdisableであるかをチェックしてHDMI_MAI_DATAにCPUで書き込むというもの。busybitは以下。
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_regs.h#L757 -
HDMI_MAI_DATAは前述のlinuxのコメントの通り、fifo扱いらしくあふれそうになるとHDMI_MAI_CTLの以下のstatus error系のbitが立って知らせてくれます
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_regs.h#L770 -
エラー状態がassert状態だとHDMI_MAI_DATAに値を書き込むときにCPUがstallして動かなくなります。HDMI_MAI_CTL側をチェックしてからHDMI_MAI_DATAに値を書き込まないといけませんでした。
-
で、こんな実装をalsaがしているわけがないので、淡々と調べていった結果DREQ使ってるじゃんというオチがわかりました。難しいですねこのあたり。
追跡していた時のメモとか以下
snd_soc_dai_init_dma_data
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/include/sound/soc-dai.h#L499
playback_dma_data
https://github.com/raspberrypi/linux/blob/ef25717980ca0a3ec85d83b99b462f329943fd33/sound/soc/codecs/hdmi-codec.c#L525
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/drivers/gpu/drm/vc4/vc4_hdmi.c#L1993
https://github.com/raspberrypi/linux/blob/ef25717980ca0a3ec85d83b99b462f329943fd33/sound/soc/codecs/hdmi-codec.c#L565
snd_pcm_fill_iec958_consumer
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/sound/core/pcm_iec958.c#L133
fill_iec958_consumer
https://github.com/raspberrypi/linux/blob/rpi-5.15.y/sound/core/pcm_iec958.c#L48
HDMI Audio+raspberry piの記事ってかなり少ない気がします、というかほぼナシ(2022/12)
よくまとまっていないけどおわり!気がむいたらまた遊ぼう(来年とか)