TC002 speaker driver¶
awtrix_pcm.ko is a GPL kernel module that drives the TC002 speaker through the MHAL_AUDIO_*
functions exported by the stock kernel module mhal.ko. Its userspace side is the static musl
helper awtrix-tc002-audio-pcm. No closed userspace library is involved. The module lives in
src/platform/tc002/kmod/awtrix_pcm/,
the helper in
src/platform/tc002/audio/pcm/,
and the build recipe is
tools/tc002/kernel/build-awtrix-pcm.sh.
How it works¶
Speaker backend selection¶
The release carries bin/awtrix-tc002-audio-pcm and lib/modules/awtrix_pcm.ko. At its start,
awtrix-tc002d loads lib/modules/awtrix_pcm.ko of the mounted release
(/tmp/awtrix-release) with finit_module in a short-lived child, unless /proc/modules already
lists awtrix_pcm. The module must be a regular file owned by root that group and others cannot
write. Loading takes 10–40 ms.
The daemon then starts awtrix-tc002-audio-pcm next to every runtime, with the runtime's end of a
SOCK_SEQPACKET socket on fd 104. The helper opens /dev/awtrix_pcm once and keeps it open,
muted between sounds. Volume follows a curve from −60 to −3 dB, and every sound ends with STOP
once only silence is left. The helper uses about 0.24 MB of RAM.
The daemon logs the outcome as speaker backend: …, and ctl status shows it under audio
(backend, backendNote, state, lastExit):
| Case | backend |
Speaker until the next boot |
|---|---|---|
module loaded (awtrix_pcm loaded in … ms) or already listed |
pcm |
awtrix_pcm helper |
| while the module loads | loading |
none yet |
the helper or the module is missing, the module is writable by others or owned by someone else, /proc/modules is unreadable, the load fails, or it has not finished after 10 s |
off, the reason in backendNote |
none |
the helper exits with anything but 0 or is killed (lastExit, e.g. exit 3 (preflight refused)) |
off |
none |
| no speaker configured | none |
none |
The display and the rest of the runtime keep running in every case.
Output ownership¶
The MHAL output device 0 has exactly one owner at a time.
awtrix-tc002druns one speaker helper at a time and starts the next one only after the previous one was reaped. Before it opens/dev/awtrix_pcm,awtrix-tc002-audio-pcmrefuses (exit 3) while the stock GUI host runs or/proc/interruptsshows anaio_dmainterrupt. mhal requestsaio_dma2inOpenPcmOutand frees it inClosePcmOut, so the interrupt is present exactly while some owner has the output open.- Loading the module touches no audio hardware; only
open()does. - Only
awtrix-tc002dloads the module. The vendor app never runs the daemon, so it never loads or opens awtrix_pcm. awtrix_pcmrefuses a second opener withEBUSY. If mhal refusesConfigPcmOutbecause another owner has the output open,open()also fails withEBUSYand nothing else is touched.- mhal is
[permanent]and awtrix_pcm sits on top of it. The daemon never unloads awtrix_pcm, so it stays loaded until the next boot.
Interface¶
/dev/awtrix_pcm is a misc character device (mode 0600, dynamic minor). It accepts one opener at
a time.
- Format:
write()takes s16le mono PCM at 44 100 Hz. The byte count must be even. Resampling stays in userspace. - Backpressure:
write()blocks until the DMA ring has room. WithO_NONBLOCKit returns a partial count orEAGAIN.poll()reportsPOLLOUTonce a full period is free, andPOLLERRafter a HAL failure. Once a HAL call has failed, the device stays failed until it is closed. - Start: playback starts once
start_bytesare queued.AWTRIX_PCM_DRAINandclose()also start a stream that is shorter than that. - Close:
close()waits for the queue to play out, at mostbuffer + 250 ms, and not at all when the process is being killed. Then the module fades the gain to −64 dB, stops the DMA and closes the MHAL output, which switches the amplifier off. - Underruns: when the queue runs empty, mhal stops the DMA and the next write restarts it.
Idle gaps count as underruns too. When the DMA level stops moving for
buffer + 250 mswhile the ring is full, the module logs a warning and restarts the stream.
ioctls¶
awtrix_pcm.h defines them.
| ioctl | Effect |
|---|---|
SET_GAIN |
Gain in dB, clamped to [−64, max_gain_db]. |
SET_MUTE |
Sets the gain to −64 dB and writes zeros for any data sent while muted. |
DRAIN |
Plays out what is queued. |
STOP |
Fades out and discards the queue. Writes after it start a new stream. |
GET_STATUS |
State, written and queued bytes, underruns, gain, the last HAL error and the measured consumption rate. |
Module parameters¶
Read-only after load. Byte values count DMA bytes, which is twice the input bytes when
channels=2.
| Parameter | Default | Meaning |
|---|---|---|
ao_dev |
0 | MHAL output device. 0 is line out, the speaker |
channels |
1 | 1: mono DMA (HAL mono mode). 2: each sample duplicated into a stereo frame (the fallback if mono mode misbehaves) |
buffer_bytes |
16384 | DMA ring, 186 ms |
period_bytes |
2048 | transfer unit, 23 ms |
start_bytes |
4096 | prefill before start, 46 ms |
initial_gain_db |
−28 | gain after open |
max_gain_db |
−3 | highest gain a client may set; the helper's volume curve ends at −3 dB |
MHAL call sequence¶
| When | Calls |
|---|---|
insmod |
None. It only allocates the 16 KiB DMA ring with dma_alloc_coherent from CMA; MIU address = physical − 0x20000000 |
open |
Init(NULL) → SetGainOut(−64, ch 0 and 1) → ConfigPcmOut(0, cfg) → OpenPcmOut(0), then the DMA level must read 0 |
write |
GetPcmOutCurrDataLen, then WriteDataOut(…, block=0) with multiples of 16 bytes and never more than free space − 256 bytes. Until the stream starts, the DMA level must equal the bytes written. At start_bytes: SetGainOut(target) → StartPcmOut |
| underrun | WriteDataOut fails while running → PrepareToRestartPcmOut → one retry |
drain end, STOP |
SetGainOut(−64) → PrepareToRestartPcmOut |
close |
SetGainOut(−64) → StopPcmOut → ClosePcmOut |
The module never calls DeInit, AmpEnable or SetDacMute. It also skips IsPcmOutXrun and every AI
(audio input) function, and it touches no device other than ao_dev.
GetPcmOutCurrDataLen and StopPcmOut dereference the HAL's per-DMA runtime data, which exists
only between OpenPcmOut and ClosePcmOut, so the module calls these two only after a
successful open.
Design basis¶
The module is written from GPL sources: the SigmaStar audio driver in
OpenIPC/u-boot-sigmastar@bf77aff5 (drivers/mstar/aio, whose audio_ut_module.c is the template
for the call order) and mhal_common.h of the Pioneer3 kernel e53dccbc. BUILD_BUG_ON pins the
40-byte layout of MHAL_AUDIO_PcmCfg_t. The build checks every import against the kernel and
mhal exports (Build).
Build¶
TC002_STOCK_MODULES=<dir with stock mhal.ko, aic8800_*.ko> \
TC002_STOCK_IMAGE=<decompressed stock Image> \
tools/tc002/kernel/build-awtrix-pcm.sh [out-dir]
The output directory defaults to $TC002_CACHE/awtrix_pcm. The script needs the tree from
prepare-tree.sh (see Kernel modules), runs build-module.sh, and checks that:
check-ko.shpasses;- every import is exported either by the kernel (
Module.symvers) or bymhal.symvers; - the MHAL imports are exactly the ten in
mhal.symvers; depends=mhal;- with the private stock files present, the MHAL imports are in the stock
mhal.koexport table and the kernel imports are in the stockImageexport table.
It also builds awtrix-pcm-ctl (static musl) and writes two test tones, both 440 Hz at −12 dBFS
with 10 ms fades: tone-440hz-300ms.raw (26 464 bytes) and tone-440hz-3s.raw. The build is
reproducible: the same source gives the same awtrix_pcm.ko SHA-256 in any directory, since the
module carries no build ID.
The helper awtrix-tc002-audio-pcm is a CMake target: the host build runs its contracts
(tc002-audio-*), and the tc002 preset builds the static musl binary and runs them again under
qemu-arm (tc002-arm-*).
Checking on a clock¶
The module reports on the kernel log (dmesg):
- On load:
awtrix_pcm: ready: ao_dev 0, 1 channel(s), buffer 16384, period 2048, start 4096 bytes, dma 0x2… (MIU 0x0…), and a nodecrw------- 10,<minor>at/dev/awtrix_pcm. - At the first stream start:
DMA level 2048 after 2048 prepared bytes. - On close:
closed: <bytes> bytes written, <n> underruns, measured ≈88200 B/s (nominal 88200), hal error 0. After the close,/proc/interruptsshows noaio_dma2line.
awtrix-pcm-ctl play FILE [GAIN_DB [STOP_AFTER_MS]] plays a raw s16le mono 44.1 kHz file such as
tone-440hz-3s.raw directly on the device node. Since the device takes one opener at a time, this
works only while no speaker helper holds /dev/awtrix_pcm.
| Log line | Meaning |
|---|---|
Unknown symbol at load |
mhal is not loaded, or its exports differ |
ENOMEM at load |
no CMA memory for the DMA ring |
ConfigPcmOut refused (…): output 0 busy or configuration rejected |
another owner holds the output |
WriteDataOut failed (-2) |
the PCM config layout does not match mhal |
DMA level does not match the written bytes, not starting |
the DMA level counter behaves differently; the DMA never ran |
OpenPcmOut failed |
mhal could not open the output |
| measured rate near 176 400 or 96 000 B/s | stereo semantics (try channels=2) or 48 kHz |
DMA level stuck at … bytes, restarting the stream |
the DMA stopped moving; the module restarted it |
In every failure at open or before the first start, the module has closed the output again and
the DMA never ran. write() sleeps interruptibly, so no process waits in the D state.
panic=5 does not catch a bus hang, which a BACH (audio block) register access while the audio
clock is off would cause; the module therefore uses only MHAL entry points.
Through the AWTRIX runtime¶
Precondition: awtrix-tc002d ctl status shows "backend":"pcm" and "state":"running" under
audio. Then:
GET /api/v1/capabilitiesreportsrtttl,mp3andradioastrueunderaudio.POST /api/v1/audio/play {"rtttl":"t:d=8,o=5,b=180:c,e,g,c6"}plays four rising notes at the alert volume.- Upload a short MP3 in the web UI, then
POST /api/v1/audio/play {"file":"<name>"}: it plays at the alert volume;GET /api/v1/audioshowsalert.playingtruewhile it plays andfalseafterwards. POST /api/v1/audio/play {"station":"http://LAN_HOST/STREAM.mp3"}: a plain HTTP MP3 station starts within about a second, and its title appears in the state. A melody played meanwhile interrupts it, and the station comes back afterwards. Decoding takes about 1.7 ms per 26 ms frame.POST /api/v1/audio/stop {}: silence within about 0.1 s.- Changing
volume,alertVolumeorradioVolumewhile playing changes the loudness. A heard level of 100 (volumeand the group both at 100) is −3 dB, the top of the helper's volume curve. - After every sound, the idle speaker is silent: no hiss, hum or buzz.
- Killing the helper leaves the display running and logs
TC002 speaker unavailable; the speaker then stays off until the next boot ("backend":"off").