Add BT1035 driver with manual chapter and expand Si4684 tuning docs.

Implement Bt1035Driver AT init (AT+AUXCFG=1), core AT parser with host tests,
wire boot into HardwareBootstrap, and document DAB/FM tuning workflows in ch-si4684.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 17:07:37 +02:00
co-authored by Cursor
parent d6fa41e944
commit d49b2011dd
22 changed files with 979 additions and 48 deletions
+3 -1
View File
@@ -98,7 +98,9 @@ persistence fails.
Returns a tuner snapshot serialised by
\texttt{core::serializeTunerStatusJson()} from \texttt{core::TunerStatus}.
The handler calls \texttt{tuner::TunerService::refreshStatus()}.
The handler calls \texttt{tuner::TunerService::refreshStatus()}. DAB and FM
tuning workflows are in Chapter~\ref{ch:si4684}, Sections~\ref{sec:si4684-dab-session}
and~\ref{sec:si4684-fm-session}.
\begin{drnote}[Response schema (DAB example)]
\begin{drcode}[JSON]
+239
View File
@@ -0,0 +1,239 @@
% ============================================================
% DigiRadio — Manual chapter: FSC-BT1035 Bluetooth
% ============================================================
\chapter{FSC-BT1035 Bluetooth Transmitter}
\label{ch:bt1035}
The Feasycom FSC-BT1035 (Qualcomm QCC3056) is the wireless output stage of
DigiRadio: it receives PCM from the ADAU1701 over I\textsuperscript{2}S and
streams Bluetooth audio with aptX, aptX~HD, and aptX~Adaptive. This chapter
documents how the ESP32-S3 controls the module over UART (AT commands with
RTS/CTS), why Line-In mode is mandatory, and how \texttt{bt1035::Bt1035Driver}
implements the bring-up sequence.
\begin{drref}[Hardware context]
Board wiring (UART pins, I\textsuperscript{2}S to the module, flow control)
is in Chapter~\ref{ch:hardware}, Section~\ref{sec:hw-bt1035}. The ADAU1701
master clock and limiter settings that feed the module are in
Chapter~\ref{ch:adau1701} and Chapter~\ref{ch:sigmastudio}.
\end{drref}
\section{Role in the audio chain}
\label{sec:bt1035-role}
The BT1035 is an I\textsuperscript{2}S \textbf{slave} at 48\,kHz: LRCLK and
BCLK come from the ADAU1701; PCM arrives on \texttt{SDATA\_OUT0}
(ADAU MP6 $\rightarrow$ module pin~5). The ESP32 does not process audio
samples for Bluetooth; it only configures the module so the wired path is
accepted and encoded for transmission.
Without firmware init the module may stay in a default mode that ignores the
Line-In from the DSP. The mandatory \texttt{AT+AUXCFG=1} command selects
auxiliary/Line-In input --- omitting it silently breaks the entire wireless
output (Section~\ref{sec:bt1035-linein}).
\section{Control interface}
\label{sec:bt1035-uart}
\subsection{UART parameters}
\begin{table}[htbp]
\centering
\begin{tabular}{@{}ll@{}}
\drhead Setting & Value \\
\midrule
Port & UART2 (not the console UART) \\
Baud rate & 115200 \\
Data format & 8N1 \\
Flow control & Hardware RTS/CTS (required) \\
Reset & GPIO active-low pulse at boot \\
SYS\_CTL & Held active to enable the module \\
\bottomrule
\end{tabular}
\caption{FSC-BT1035 UART and control lines (\texttt{board\_pins.hpp}).}
\label{tab:bt1035-uart}
\end{table}
\subsection{Response handling}
Every command expects a module reply containing \texttt{OK} or
\texttt{ERROR}. The driver:
\begin{itemize}
\item builds lines in the pure core via \texttt{core::buildBt1035AtLine()};
\item classifies replies with \texttt{core::parseBt1035AtResponse()};
\item treats timeouts and unexpected payloads as errors (never ignored).
\end{itemize}
Host tests in \texttt{components/core/test/bt1035\_at\_test.cpp} lock the
init sequence (including \texttt{AT+AUXCFG=1}) and the parser.
\section{Mandatory Line-In mode}
\label{sec:bt1035-linein}
\begin{drcaution}[AT+AUXCFG=1 is not optional]
The documented init sequence must include \texttt{AT+AUXCFG=1} after a
successful \texttt{AT} ping. This tells the QCC3056 firmware to take audio
from the wired I\textsuperscript{2}S/Line-In port (the ADAU1701 output)
rather than an internal source. AGENTS.md and the hardware manual both treat
skipping this step as a production bug.
\end{drcaution}
\section{Boot sequence}
\label{sec:bt1035-boot}
At power-up \texttt{HardwareBootstrap::boot()} runs the Si4684 and ADAU1701
first, applies the saved audio profile, then initialises the BT1035 so the
Line-In path is ready before Wi-Fi starts.
\begin{figure}[htbp]
\centering
\begin{tikzpicture}[
step/.style={draw, rounded corners, minimum width=9.5cm,
minimum height=0.9cm, align=center, font=\small},
>={Latex}, node distance=3mm]
\node[step, fill=black!6] (sys) {SYS\_CTL high, RESET\# pulse};
\node[step, fill=black!6, below=of sys] (uart)
{Install UART2 @ 115200, RTS/CTS};
\node[step, fill=black!8, below=of uart] (at)
{Send \texttt{AT} --- expect OK};
\node[step, fill=black!10, below=of at] (aux)
{Send \texttt{AT+AUXCFG=1} --- expect OK (Line-In)};
\node[step, fill=black!6, below=of aux] (done)
{\texttt{Bt1035Driver::isBooted()} = true};
\foreach \a/\b in {sys/uart, uart/at, at/aux, aux/done} {
\draw[->] (\a) -- (\b);
}
\end{tikzpicture}
\caption{FSC-BT1035 AT init on DigiRadio.}
\label{fig:bt1035-boot}
\end{figure}
Pairing, codec selection, and volume over Bluetooth are handled by the
module's own firmware and NVS; DigiRadio firmware currently implements
only the Line-In bring-up required for the wired audio path.
\section{Software architecture}
\label{sec:bt1035-stack}
\begin{table}[htbp]
\centering
\small
\begin{tabular}{@{}llp{5.8cm}@{}}
\drhead Layer & Type & Responsibility \\
\midrule
Bootstrap & \texttt{HardwareBootstrap} &
Constructs driver, calls \texttt{boot()} after ADAU1701 \\
Driver & \texttt{bt1035::Bt1035Driver} &
UART, reset, AT init sequence \\
Pure core & \texttt{core::Bt1035AtCommand}, parser &
Host-testable command strings and OK/ERROR classification \\
\bottomrule
\end{tabular}
\caption{BT1035 control stack (Slice~6). Future HTTP/UI for pairing will
sit above the driver without changing the init contract.}
\label{tab:bt1035-stack}
\end{table}
\section{Supported AT command subset}
\label{sec:bt1035-at}
The firmware enumerates every command it sends. Extending the subset requires
updating \texttt{core::Bt1035AtCommand}, the manual, and a host test.
\begin{table}[htbp]
\centering
\begin{tabular}{@{}lll@{}}
\drhead Enum & Line sent & Purpose \\
\midrule
\texttt{Ping} & \texttt{AT} & Verify UART link \\
\texttt{AuxLineIn} & \texttt{AT+AUXCFG=1} & Enable Line-In from ADAU \\
\bottomrule
\end{tabular}
\caption{AT commands used at boot (\texttt{core::bootInitSequence()}).}
\label{tab:bt1035-at}
\end{table}
\section{Bt1035Driver API}
\label{sec:bt1035-driver}
\texttt{Bt1035Driver} owns UART and GPIO reset. All methods return
\texttt{std::expected<T, Bt1035Error>}.
\begin{table}[htbp]
\centering
\begin{tabular}{@{}ll@{}}
\toprule
\textbf{Method} & \textbf{Purpose} \\
\midrule
\texttt{boot()} & Reset, UART init, run \texttt{bootInitSequence()} \\
\texttt{isBooted()} & \texttt{true} after Line-In init succeeded \\
\texttt{sendCommand(cmd)} & Send one typed command, expect OK \\
\bottomrule
\end{tabular}
\caption{Public driver API.}
\label{tab:bt1035-api}
\end{table}
\subsection{Error codes}
\begin{table}[htbp]
\centering
\small
\begin{tabular}{@{}ll@{}}
\drhead \texttt{Bt1035Error} & Typical cause \\
\midrule
\texttt{ResetFailed} & GPIO configuration failure \\
\texttt{UartInitFailed} & \texttt{uart\_driver\_install} / pins \\
\texttt{NotBooted} & \texttt{sendCommand} before \texttt{boot()} \\
\texttt{AtTimeout} & No OK/ERROR within 2\,s \\
\texttt{AtError} & Module returned ERROR \\
\texttt{UnexpectedResponse} & Unrecognised payload (future use) \\
\bottomrule
\end{tabular}
\caption{Driver error enumeration.}
\label{tab:bt1035-errors}
\end{table}
\section{Integration at power-up}
\label{sec:bt1035-integration}
\texttt{main/hardware\_bootstrap.cpp} constructs a static
\texttt{Bt1035Driver} and calls \texttt{boot()} after
\texttt{AudioService::loadAndApply()}. Failure returns
\texttt{HardwareBootError::Bt1035BootFailed} and \texttt{app\_main} halts
before network bring-up (fail-closed, same as Si4684/ADAU1701).
Boot order:
\begin{enumerate}
\item Si4684 \texttt{boot(Dab)} --- tuner image in RAM.
\item ADAU1701 \texttt{boot()} --- SigmaStudio program in RAM.
\item \texttt{AudioService::loadAndApply()} --- user mixer/EQ profile.
\item BT1035 \texttt{boot()} --- Line-In enabled for wireless output.
\end{enumerate}
\section{Typical usage (firmware developer)}
\label{sec:bt1035-usage}
After a successful \texttt{HardwareBootstrap::boot()}, the module is ready;
no further calls are required for basic listening. To re-send Line-In config
after a module reset:
\begin{verbatim}
bt1035::Bt1035Driver& bt = ...;
if (auto r = bt.sendCommand(core::Bt1035AtCommand::AuxLineIn); !r) {
// handle Bt1035Error
}
\end{verbatim}
\section{Further reading}
\label{sec:bt1035-reading}
\begin{itemize}
\item Feasycom FSC-BT1035 AT command manual (vendor) --- full command set
for pairing, name, and codec options not yet wrapped by firmware.
\item Chapter~\ref{ch:hardware} --- pin map and I\textsuperscript{2}S routing.
\item Chapter~\ref{ch:adau1701} --- DSP output that feeds the module.
\item \texttt{components/core/test/bt1035\_at\_test.cpp} --- init sequence test.
\end{itemize}
+7 -1
View File
@@ -130,7 +130,13 @@ fetch, and \texttt{startDabService}. Property access (\texttt{setProperty},
\texttt{setVolume}) and diagnostics (\texttt{getPartInfo}, \texttt{getSysState})
are included. Wrong-band calls return \texttt{Si4684Error::WrongBand}.
Register-level opcodes remain private; see \texttt{Si4684Types.hpp} for status
DTOs.
DTOs. Full DAB/FM tuning guide: Chapter~\ref{ch:si4684}.
\section{Bt1035Driver}\label{cls:Bt1035Driver}
UART driver for the FSC-BT1035 (Chapter~\ref{ch:bt1035}). \texttt{boot()}
pulses RESET\#, opens UART2 with RTS/CTS, and runs
\texttt{core::bootInitSequence()} (Ping + \texttt{AT+AUXCFG=1}). Returns
\texttt{Bt1035Error} on timeout, ERROR response, or UART failure.
\section{Adau1701Driver}\label{cls:Adau1701Driver}
RAII I\textsuperscript{2}C driver for the ADAU1701 SigmaDSP
+8 -4
View File
@@ -147,10 +147,11 @@ safeload registers.
\end{drcaution}
\paragraph{FSC-BT1035 (Bluetooth).}
The module is controlled by AT commands over UART. The initialisation
sequence includes enabling Line-In mode (\texttt{AT+AUXCFG=1}), which is
required for the wired audio path from the DSP; command responses are
parsed explicitly, with timeouts treated as errors.
The module is controlled by AT commands over UART with hardware flow control.
The initialisation sequence includes enabling Line-In mode
(\texttt{AT+AUXCFG=1}), which is required for the wired audio path from the
DSP; command responses are parsed explicitly, with timeouts treated as errors.
Full driver API, boot flow, and error codes are in Chapter~\ref{ch:bt1035}.
\section{Configuration, storage, and user interface}
\label{sec:fw-config}
@@ -203,6 +204,9 @@ bring-up:
\item \textbf{Audio profile}: \texttt{audio::AudioService::loadAndApply()}
restores the saved \texttt{core::AudioProfile} from NVS (or factory
defaults) via ADAU1701 safeload before network bring-up.
\item \textbf{FSC-BT1035} (UART): \texttt{bt1035::Bt1035Driver::boot()}
enables Line-In (\texttt{AT+AUXCFG=1}) after the DSP path is configured
(Chapter~\ref{ch:bt1035}).
\end{enumerate}
If either driver returns an error, the firmware logs the failure and stops
+2 -1
View File
@@ -131,7 +131,8 @@ in the module's own non-volatile memory.
\begin{drcaution}[Line-In mode]
The initialisation sequence must enable Line-In mode (\texttt{AT+AUXCFG=1})
so the module accepts the wired audio coming from the DSP. Omitting it
silently breaks the audio path.
silently breaks the audio path. Driver boot flow and AT subset are documented
in Chapter~\ref{ch:bt1035}.
\end{drcaution}
\section{The host: ESP32-S3}
+2 -1
View File
@@ -30,7 +30,8 @@ extending DigiRadio: the hardware at a block level
(Chapter~\ref{ch:hardware}), the firmware architecture
(Chapter~\ref{ch:firmware}), dedicated companion-chip guides for the
Si4684 tuner (Chapter~\ref{ch:si4684}) and ADAU1701 DSP
(Chapter~\ref{ch:adau1701}), the HTTP JSON API exposed by the web UI
(Chapter~\ref{ch:adau1701}), FSC-BT1035 Bluetooth
(Chapter~\ref{ch:bt1035}), the HTTP JSON API exposed by the web UI
(Chapter~\ref{ch:api}), the per-class design reference that grows with
the code (Chapter~\ref{ch:classes}), and how to build and flash
(Chapter~\ref{ch:build}). Exact C++ signatures are generated by Doxygen
+88 -2
View File
@@ -224,18 +224,104 @@ after boot.
\end{table}
\subsection{Typical DAB session}
\label{sec:si4684-dab-session}
\begin{enumerate}
\item \texttt{boot(Dab)} at power-up (already done in
\texttt{HardwareBootstrap}).
\item \texttt{tuneDab(index)} for the desired ensemble.
\item \texttt{tuneDab(index)} for the desired ensemble (Band III index
0--37, Table~\ref{tab:si4684-dab-plan}).
\item Poll \texttt{readDabDigRadStatus()} until FIC quality $> 0$.
\item \texttt{fetchDabServiceList()} when
\texttt{readDabEventStatus().serviceListReady}.
\item \texttt{startDabService(serviceId, componentId)} for the chosen
programme; audio appears on I\textsuperscript{2}S.
programme; PCM appears on I\textsuperscript{2}S to the ADAU1701.
\end{enumerate}
\begin{drnote}[DAB, not DVB]
DigiRadio receives \textbf{DAB/DAB+} (Digital \emph{Audio} Broadcasting),
not DVB-T/T2 video. Tuning is by ensemble frequency index and digital
service/component IDs --- there is no transport-stream PAT/PMT scan like
DVB-T.
\end{drnote}
\subsection{Typical FM session}
\label{sec:si4684-fm-session}
FM requires the FM application image loaded at boot
(\texttt{boot(Si4684Band::Fm)}). DigiRadio defaults to DAB at power-up;
switching to FM reloads patch + \texttt{fm\_firmware.bin} (full HOST\_LOAD).
\begin{enumerate}
\item \texttt{boot(Fm)} --- resets the chip and loads the FM image.
\item \texttt{tuneFm(frequencyKhz)} --- e.g.\ 101500 for 101.5\,MHz;
waits for seek/tune complete (STC).
\item \texttt{readFmRsq()} --- RSSI, SNR, stereo flag, validity.
\item Optional: \texttt{seekFm(up, wrap)} --- scan to next station.
\item Optional: \texttt{readFmRds()} --- last RDS group (PI, PS, RT).
\end{enumerate}
FM audio is emitted on the same I\textsuperscript{2}S pins as DAB once the
FM image is running.
\subsection{Choosing DAB or FM}
\label{sec:si4684-band-choice}
Only one application image runs at a time. \texttt{Si4684Driver::boot(band)}
selects the blob:
\begin{table}[htbp]
\centering
\begin{tabular}{@{}lll@{}}
\drhead Band & Image loaded & Primary API \\
\midrule
\texttt{Si4684Band::Dab} & \texttt{dab\_firmware.bin} &
\texttt{tuneDab}, service list, \texttt{startDabService} \\
\texttt{Si4684Band::Fm} & \texttt{fm\_firmware.bin} &
\texttt{tuneFm}, \texttt{seekFm}, RSQ, RDS \\
\bottomrule
\end{tabular}
\caption{Band-specific firmware and driver entry points.}
\label{tab:si4684-band-api}
\end{table}
Calling an FM method while the DAB image is loaded (or vice versa) returns
\texttt{Si4684Error::WrongBand}. Switching band performs reset and a complete
HOST\_LOAD of patch + the other image ($\sim$1\,s).
\subsection{HTTP API mapping (web UI)}
\label{sec:si4684-http}
The setup UI and REST clients use \texttt{tuner::TunerService}, which wraps
\texttt{Si4684Tuner} (Chapter~\ref{ch:api}). Summary:
\begin{table}[htbp]
\centering
\small
\begin{tabular}{@{}llp{5.5cm}@{}}
\drhead Endpoint & Band & Action \\
\midrule
\texttt{POST /api/tuner/tune} &
DAB & \texttt{\{"band":"dab","freq\_index":0..37\}} \\
\texttt{POST /api/tuner/tune} &
FM & \texttt{\{"band":"fm","frequency\_khz":64000..108000\}} \\
\texttt{GET /api/tuner/services} & DAB only & Programme list for ensemble \\
\texttt{POST /api/tuner/play} & DAB only & Start \texttt{service\_id}/\texttt{component\_id} \\
\texttt{POST /api/tuner/seek} & FM only & Seek up, return new kHz \\
\texttt{GET /api/tuner/status} & both & Locked state, RSQ or DIGRAD \\
\bottomrule
\end{tabular}
\caption{Tuner HTTP routes vs band (full schemas in Chapter~\ref{ch:api}).}
\label{tab:si4684-http}
\end{table}
\begin{drnote}[FM tune via HTTP today]
\texttt{POST /api/tuner/tune} with \texttt{band:"fm"} calls
\texttt{TunerService::tuneFm}, which may trigger a band reload if the device
booted in DAB. Plan station presets and automatic band selection in a later
slice.
\end{drnote}
\section{Integration at power-up}
\label{sec:si4684-integration}
+1
View File
@@ -48,6 +48,7 @@
\include{ch-firmware}
\include{ch-si4684}
\include{ch-adau1701}
\include{ch-bt1035}
\include{ch-api}
\include{ch-classes}
\include{ch-build}