Repository navigation
CSS
Chirp spread spectrum, like LoRa but in the audio band: each symbol is a chirp, a tone whose frequency rises linearly over the bandwidth (1000 Hz around 1500 Hz by default). The symbol value is the frequency at which the chirp starts: when it reaches the upper band edge, it continues at the lower edge. With a spreading factor SF there are 2^SF start frequencies, so each symbol carries SF bits. The receiver multiplies the chirp with the conjugate chirp (dechirping), which turns it into a single tone, and finds its frequency with an FFT. CSS is an extension of TinyModem: it is not compatible with LoRa radios or other programs.
Advantages
- Very robust:
css9decodes about 12 dB below the noise (in 3 kHz bandwidth), similar to Olivia. - Each step of the spreading factor doubles the symbol duration and improves the sensitivity by about 2.5 dB: you can trade speed against robustness.
- Robust against frequency offsets (±60 Hz with
css7) and different clocks (see below). - The data is sent in frames with a length header, a CRC-16 and Reed-Solomon error correction.
Disadvantages
- Slow: 10 to 156 bit/s.
- The data is sent in frames of up to 200 bytes: there is a delay until a frame is full or
flush()is called. - The receiver needs more CPU than the simple modems (an FFT of 2 × 2^SF points per symbol).
- Not compatible with LoRa or other programs.
| Mode | SF | Bandwidth | Symbol | Data rate |
|---|---|---|---|---|
css5 |
5 | 1000 Hz | 32 ms | 156 bit/s |
css6 |
6 | 1000 Hz | 64 ms | 94 bit/s |
css7 |
7 | 1000 Hz | 128 ms | 55 bit/s |
css8 |
8 | 1000 Hz | 256 ms | 31 bit/s |
css9 |
9 | 1000 Hz | 512 ms | 18 bit/s |
css10 |
10 | 1000 Hz | 1024 ms | 10 bit/s |
css is css7. The bandwidth can be added to the name, e.g. css9-500 or css6-2000: half the bandwidth doubles the symbol duration (half the data rate) and narrows the signal. The data rate is the raw rate: the header, the CRC and the Reed-Solomon parity (8 bytes per frame by default) come on top.
I2SStream i2s;
CSSStream modem(i2s, RXTX_MODE);
void setup() {
CSSConfig cfg = modem.defaultConfig(); // css7
cfg.sample_rate = 16000;
...
modem.begin(cfg);
}
void loop() {
modem.println("Hello");
modem.flush(); // sends the frame
while (modem.available()) Serial.write(modem.read());
}echo "Hello" | tinymodem --tx -f hello.wav css7
tinymodem --rx -f hello.wav css7CSSConfig, CSSEncoder, CSSDecoder, CSSStream and CSSStreamRTOS are the CSS classes. The encoder collects the written bytes and sends a frame when max_frame bytes were written or when flush() (or endTransmission()) is called. On the command line, -R, -v, -M (center frequency), -q, --rx-one, --packets and --Xrxnoise are supported.
CSSConfig field |
Default | |
|---|---|---|
spreading_factor |
7 | Bits per symbol (5 - 10) |
bandwidth |
1000 | Bandwidth of the chirps (Hz) |
center_freq |
1500 | Center frequency (Hz) |
volume |
1.0 | Transmit amplitude |
preamble_symbols |
8 | Transmitter: up chirps before the sync symbols |
max_frame |
64 | Transmitter: maximum number of bytes per frame (1 - 200) |
ecc_bytes |
8 | Reed-Solomon parity bytes per frame (2 - 64): corrects ecc_bytes / 2 wrong bytes. Sender and receiver must use the same value |
detect_threshold |
8.0 | Receiver: min ratio of the peak power to the mean power of the other FFT bins |
rx_one |
false | Receiver: stop after the first frame |
channel |
0 | Receiver: input channel |
sample_rate, channels, bits_per_sample
|
48000, 1, 16 | Audio format |
A frame consists of:
- The preamble: 8 up chirps with the value 0.
- Two down chirps: the frequency falls over the bandwidth. A time offset moves the peak of the up chirps and the down chirps in opposite directions, a frequency offset in the same direction: so the receiver can determine both from the peaks.
- The header: the length of the frame with 4 Reed-Solomon parity bytes.
- The payload: the data, a CRC-16 and
ecc_bytesReed-Solomon parity bytes.
The bytes are sent as SF bits per symbol, most significant bit first. The symbol value is the inverse Gray code of the bits: the most likely error (the neighboring FFT bin) only changes one bit.
The receiver mixes the audio to the complex baseband and converts it to twice the bandwidth (2 samples per chip) with a windowed sinc filter. The FFT of the dechirped window has 2 × 2^SF bins: the part of the chirp before the wrap around and the part after it are separate tones, whose power is added. This makes the detection robust against any time offset of the window. The fractional position of the peak is calculated with Quinn's estimator.
The preamble is detected when 3 consecutive windows have their peak in the same bin. Then the window is aligned to the symbols and the receiver waits for the down chirps, which provide the time and frequency offset. The change of the preamble peaks from window to window is the drift caused by a different clock of the sender: if it is clearly measurable, the receiver resamples the following symbols with the clock of the sender. During the frame a second order loop follows the remaining frequency offset and drift, and the window is moved by a whole chip when the timing has drifted by one chip.
Measured with the unit tests and the command line tool at 8000 Hz:
-
Noise: added uniform noise (
--Xrxnoise) up to 3 (css7) or 6 (css9) is decoded without errors: about -6.5 dB and -12.5 dB SNR in 3 kHz bandwidth. Beyond that, frames are lost. -
Frequency offset: ±60 Hz (
css7). - Different clocks of sender and receiver (sound cards usually differ by less than 0.01%):
| Mode | Clock difference |
|---|---|
css5, css6
|
±1% |
css7, css8
|
±0.2 - 0.3% |
css9 |
±0.1% |
css10 |
±0.05% |
-
Speed at 8000 Hz: modulation and demodulation about 5 × real time on the ESP32-S3 and 7 - 8 × real time on the ESP32-P4 (
css7andcss10); the RP2040 demodulates just in real time (1.0), without headroom, see Performance. -
Over the air on the ESP32-P4 (speaker to the onboard microphone of the same board):
css7andcss9were received without errors.