16г. MP3 module DFPlayer Mini over UART: command frame, checksum, track queue, volume and module replies
A voice prompt, a doorbell, an alarm spoken in words — many devices need sound, and an MP3 decoder of your own will not fit into an F103. The ready-made answer is the DFPlayer Mini module: it carries a microSD slot, a decoder and a speaker amplifier. The microcontroller is left with a UART at 9600 baud and short frames of 10 bytes: "play folder 1, file 3", "volume 20", "pause". The module answers with the same kind of frames and reports on its own that a track has finished, the card was pulled out or a command was not understood.
What is inside: which modules exist and how to lay out the files on the card; the command frame and its checksum; commands and queries; parsing replies and events without waiting in a loop; a playlist on top of the "track finished" events — and why clones send that event twice, while the BUSY pin does not replace it; clicks and supply noise; pitfalls.
Foundations: the UART and the levels come from 16 and 16.7, the receive
ring filled by an interrupt and non-blocking parsing work as in 106г.3, the
split into a driver and a model is the same as in 16в. Your own sound over
I2S with a codec is in 18а. There is no document on the DFPlayer Mini or the
YX5200 chip in the project: command, reply and error codes, pin functions,
voltages and the behaviour of clones are taken from memory of the
manufacturer's documentation and marked "not verified". Everything called a
run in this section is the driver _verify/mp3_16e/dfplayer.c on a
computer (gcc 12.2.0) with a model of the module in Python 3.11; the
firmware was built with armclang 6.24 for Cortex-M3. The module's delays in
the run are model parameters, not measurements.
What it is called in the documentation
Original Meaning command frame: start, version, length, command, feedback, parameter, checksum, end the frame fields in order feedback (ACK) acknowledgement that a frame was received track, folder a track (file), a folder on the card play mode, repeat playback mode, repeat equalizer (EQ) ready-made tone presets BUSY pin the "playing" pin online devices storage media the module sees track finished, card inserted / removed the module's unsolicited events
16г.1 The module: YX5200 and clones, the card, folders, outputs and BUSY
What is inside. The DFPlayer Mini board carries a decoder chip (the original YX5200, later YX5300 and YX6300; clones use other chips with the same command set), a microSD slot and a speaker amplifier, from memory up to 3 W. Besides the speaker pins (SPK1, SPK2) there is a DAC output for an external amplifier (DAC_L, DAC_R), the BUSY pin, the UART (RX, TX) and power. Pin names and count follow the manufacturer's documentation, not verified.
The card. The card is FAT16 or FAT32. Files go into folders 01…99
named 001.mp3, 002.mp3 and so on; then the "folder/file" command finds
a file by its name. There is a second numbering as well — the global track
number, and it follows the order in which the files were written to the
card, not their names (16г.3). File system behaviour and number limits
are from memory, not verified.
Supply and levels. The module is powered from 3.3…5 V; from 5 V it is louder and the amplifier pulls the supply down less (from memory, not verified). The module's UART is a 3.3-volt one. With the F103 the lines are connected directly, but a 1 kΩ resistor is put into the line from the microcontroller TX to the module RX — the documentation recommends it to suppress noise (from memory, not verified). Ground is common, the TX of one side goes to the RX of the other (16.7).
BUSY. The pin is low while a track plays and high when it is quiet. It goes to input PB5 with a pull-up. Below you will see why it is only a backup sign of the end of a track (16г.5).
16г.2 The command frame and the checksum
All communication is in frames of 10 bytes in both directions, 9600 baud,
8N1. On an F103 at 72 MHz BRR = 7500 exactly; a byte takes 1041.7 µs, a
frame takes 10.4 ms.
| Byte | Value | Meaning |
|---|---|---|
| 0 | 7E |
start |
| 1 | FF |
version |
| 2 | 06 |
length: six bytes from the version to the parameter |
| 3 | CMD | command |
| 4 | FB | 1 — send acknowledgement 0x41, 0 — do not |
| 5, 6 | PAR1, PAR2 | parameter, high byte first |
| 7, 8 | CHK_H, CHK_L | checksum |
| 9 | EF |
end |
The checksum is 16 bits: zero minus the sum of bytes 1 to 6, that is from
FF to PAR2. Then the sum of these six bytes together with CHK gives zero
modulo 65 536. The format follows the manufacturer's documentation, not
verified; the arithmetic is checked by the run — C against an independent
Python implementation:
uint16_t df_sum(const uint8_t *f)
{
uint16_t s = 0;
for (uint8_t i = 1; i < 7; i++) /* FF 06 CMD FB PAR1 PAR2 — без 7E, суммы и EF */
s = (uint16_t)(s + f[i]);
return (uint16_t)(0u - s);
}
| Command | Frame | Sum of bytes 1…6 |
|---|---|---|
| track 1, no acknowledgement | 7E FF 06 03 00 00 01 FE F7 EF |
0x109 |
| track 1, FB = 1 | 7E FF 06 03 01 00 01 FE F6 EF |
0x10A |
| volume 20, FB = 1 | 7E FF 06 06 01 00 14 FE E0 EF |
0x120 |
| folder 2, file 3, FB = 1 | 7E FF 06 0F 01 02 03 FE E6 EF |
0x11A |
| reset | 7E FF 06 0C 00 00 00 FE EF EF |
0x111 |
| status query | 7E FF 06 42 00 00 00 FE B9 EF |
0x147 |
The "track 1" frame is the one usually quoted in examples for this module; the C and Python frames matched byte for byte.
16г.3 Commands: track, folder, volume, equalizer, pause, reset, queries
The codes follow the manufacturer's documentation, not verified; the bench model understands exactly these:
| CMD | Parameter | What it does |
|---|---|---|
01, 02 |
— | next, previous track |
03 |
1…number of files | track by its global number |
06 |
0…30 | volume |
07 |
0…5 | equalizer: normal, pop, rock, jazz, classic, bass |
0C |
— | module reset |
0D, 0E |
— | play (resume), pause |
0F |
folder, file | folder 01…99, file 001… |
16 |
— | stop |
42, 43, 44 |
— | query: status, volume, equalizer |
48, 4C |
— | query: files on the card, current track number |
4E |
folder | query: files in a folder |
The global number is the write order. The model's card holds six
files written in the order 01/002, 01/001, 01/003, 02/001,
02/002, 03/001. The run: command 03 with numbers 1, 2, 3, 4 started
01/002, 01/001, 01/003, 02/001 — number 1 went to the file written
first, not to 001.mp3. Command 0F with folder 1 and file 1 started
01/001. Query 48 returned 6, 4E for folder 1 returned 3. Hence the
rule: choose a track by folder and file, and leave the global number
for "play everything in a row".
Volume. 0…30; the driver itself clips anything above 30:
df_volume(35) went out as frame 7E FF 06 06 01 00 1E FE D6 EF, and query
43 returned 30. What a real module does with a number above 30 is not
verified.
Queries answer with a frame carrying the same code: 43 — the volume
in PAR2, 48 — the number of files. The driver does not ask for
acknowledgement 41 on a query: the value itself is the answer.
16г.4 Module replies and events: parsing the stream without blocking
Three kinds of frames come from the module: acknowledgement 41, the
answer to a query and events nobody asked for. Events and errors follow the
manufacturer's documentation, not verified:
| CMD | Parameter | When |
|---|---|---|
3F |
media: 2 — card | the module is ready after power-up or reset |
3A, 3B |
medium | card inserted, removed |
3D |
track number | a track from the card finished |
40 |
error code | 1 — busy, 2 — asleep, 3 — frame error, 4 — wrong checksum, 5 — number out of range, 6 — no such file |
41 |
— | acknowledgement of a frame with FB = 1 |
The RXNE interrupt puts bytes into a ring, and df_poll in the main loop
assembles them into frames — like the ring and lines in 106г.3, only
instead of an end of line there is a length of 10 and a check of the frame
shape. A broken frame does not drag the next one down with it: the start
is searched for again inside what has been collected:
if (d->f[1] != 0xFF || d->f[2] != 0x06 || d->f[9] != 0xEF) {
d->n_junk++;
for (uint8_t i = 1; i < 10; i++)
if (d->f[i] == 0x7E) {
memmove(d->f, d->f + i, (size_t)(10u - i));
d->fn = (uint8_t)(10u - i);
break;
}
return;
}
if ((uint16_t)(d->f[7] << 8 | d->f[8]) != df_sum(d->f)) {
d->n_badsum++;
if (!d->lax && !d->n_good && ++d->badsum_run >= DF_BADSUM_N) {
d->lax = 1; /* целые кадры, ни одной верной суммы — клон считает иначе */
emit(d, DF_EV_MODE, DF_MODE_LAX, 0);
}
if (!d->lax)
return;
} else {
d->n_good++;
d->badsum_run = 0;
}
One command in flight. The driver keeps a queue of 8 commands and sends
the next one only when the previous one has finished: 41 arrived, an
answer to a query or error 40. No answer within 200 ms — a repeat, up to
3 attempts in all. Errors 3 and 4 — the module received the frame
corrupted — are repeated at once; error 1 "busy" — repeated after 250 ms,
up to 12 times; the rest mean the command was not executed, and that is an
event for the application.
Run: power-up. The model is deaf for 1500 ms after power-up, then sends
3F with parameter 2. The driver waits for this frame (no longer than
3000 ms) and only then sends volume, equalizer and the query for the number
of files: 3F arrived after 1511 ms, the setup finished at 1637 ms. Each
command was acknowledged after 35…49 ms — 10.4 ms for the frame there, 10…30
ms of processing in the model and the frame back. The receive ring never held
more than 1 byte: with polling once per 1 ms a byte at 9600 baud arrives
less often.
16г.5 The playback queue: a playlist, "track finished" twice, BUSY versus events
A playlist is an array of "folder, file" pairs. The driver queues 0F for
the first item; on event 3D — for the next one. An item missing from the
card (error 6) is skipped. Pause and resume are 0E and 0D.
Run: a playlist. Six items, the fifth (02/005) is not on the card,
in the middle of the second track a pause of 2000 ms. 01/001, 01/002,
01/003, 02/001, 03/001 played in order: five ends by event, one
skip, none by BUSY. From the event to the sound of the next track
122…163 ms passed. During the pause BUSY stayed high for 2000 ms, and the
driver did not count it as the end of the track — the BUSY backup path is
off during a pause.
The event twice. People often write about modules with clone chips that
3D comes twice in a row (from memory, not verified). The "clone" model
sends the second 3D 10…60 ms after the first. If the next item is simply
started on it, the playlist jumps over a track. The protection is a guard
interval: during the first 1000 ms after a track starts, a "finished" event
is not about it:
static void track_end(df_t *d, uint16_t par)
{
if (!d->use_events)
return;
if (d->st != DF_ST_PLAY || (d->dedup && !after(d->now, d->play_at + DF_GUARD_MS))) {
d->n_stale++; /* второй раз о том же или о прежнем треке */
emit(d, DF_EV_STALE, par, 0);
return;
}
d->n_end_ev++;
finished(d, DF_END_EVENT);
}
| "Twice" clone, 6 items | without guard | with guard |
|---|---|---|
| played | 4 of 6: 01/001, 01/003, 02/002, 03/001 |
6 of 6 |
| extra events dropped | 0 | 6 |
The price: a track shorter than 1 s cannot end by event. Comparing the
number in 3D with the number of the started track is impossible: the event
carries the global number (write order), while the track was started by
folder and file.
BUSY versus events. BUSY does not say which track ended; it is high both during a pause and after a command while the sound has not started yet (in the model 60…120 ms). So the main sign is the event, and BUSY is the backup: while playing, later than the guard interval, a high level for longer than 500 ms means "the track ended, and the event was lost":
/* BUSY: 0 — играет. При игре 1 дольше DF_BUSY_MS — трек кончился, а событие пропало */
if (d->st == DF_ST_PLAY && busy_pin && after(now, d->play_at + DF_GUARD_MS)) {
if (!d->busy_hi) {
d->busy_hi = 1;
d->busy_since = now;
} else if (after(now, d->busy_since + DF_BUSY_MS)) {
d->n_end_busy++;
finished(d, DF_END_BUSY);
}
} else {
d->busy_hi = 0;
}
A run of the same 6-track playlist: by events the pause between tracks is 128…136 ms, by BUSY alone without events it is 598…633 ms.
A clone without acknowledgements. The model of such a clone sends no
41 and no 3F at power-up. The driver waited 3000 ms and started without
3F; the very first command went unanswered three times — in 600 ms, and
since there had been no 41 at all, the driver stopped asking for
acknowledgement and from then on waits 100 ms after each command. All 6
tracks played.
A clone with its own checksum. The model computes the checksum of its
frames without the FF byte and does not check the other side's sum. The
driver dropped two frames, and on the third intact frame with a foreign sum
— with not a single correct one before — stopped checking the sum (3245 ms
after power-up). All 6 tracks played. The price: with such a module a frame
corrupted by noise cannot be told from a correct one.
The card was pulled out. In the middle of the third track the module
sent 3B, the driver cleared the queue and went into "no card". After
2000 ms the card was inserted: 3A, the driver started the same item from
the beginning; while reading the card the module answered "busy" three times,
and the track sounded 977 ms after insertion. The playlist played to the
end.
Noise. The model corrupts each byte on the wires with probability 0.005 in both directions. A playlist of 30 tracks in a loop, the volume changes every 10 tracks, 100 track ends:
| What | Run |
|---|---|
| track ends by event / by BUSY / skips | 95 / 5 / 0 |
| track order | intact |
| commands sent / repeats / unanswered | 122 / 11 / 8 |
| errors from the module (wrong checksum) | 4 |
| frames with a wrong sum / junk at the driver | 8 / 13 |
track restarted by the same 0F |
6 |
| experiment time | 248 984 ms |
Five "track finished" events did not arrive intact — BUSY found those track
ends. One 0F command went unanswered three times; the driver did not throw
the item away but left the decision to BUSY, and the track played. Six
times an acknowledgement was lost, and the repeat restarted the same track
from the beginning — unpleasant, but the order is intact. Had the command
been "next" (01) instead of 0F, such a repeat would have jumped over a
track: a repeated command must be harmless when repeated.
16г.6 Supply and sound: clicks, noise, common ground
Everything here is qualitative: the module was not connected, there are no numbers in the project.
Clicks. The speaker amplifier draws current in bursts in time with the sound. If the module is fed from the same wire as the microcontroller, the dips go into the board supply and the UART ground. Typical consequences: a click at power-up and at the start of a track, crackling in loud passages, frame errors at the moment of a loud sound. Remedies: give the module its own supply or at least a separate wire from the source and an electrolytic capacitor at its supply pins; do not keep the volume at the maximum if the source is weak.
Common ground. There is one ground, but the speaker current must not flow through the ground of the UART and the microcontroller: the module supply wires run to the source separately, and only the signals and one ground go to the microcontroller board. The speaker output is a bridge (SPK1 and SPK2), neither of them may be tied to ground (from memory of the documentation, not verified).
External amplifier. The module's DAC output is for an amplifier of your own; ground noise is heard here more than on the built-in one. If the sound must be of good quality and generated by you rather than played from a file on the card — I2S and a codec (18а).
The 1 kΩ resistor in the line to the module RX limits the current through the input protection diodes and, together with the input capacitance, smooths the edges (from memory of the documentation, not verified).
16г.7 Pitfalls
The module stays silent on any command. TX and RX are swapped, there is no common ground (16.7), the wrong baud rate: the module needs 9600 baud. Or the command left too early after power-up: the module is still reading the card.
Volume and equalizer were not applied after a reset. The commands left
right after 0C while the module was rebooting. In the run without waiting
for 3F: 8 timeouts without an answer, volume and equalizer were sent 3
times each and lost, the module stayed at its own 30, while the driver
believed the volume was 20. With waiting for 3F the module is ready
1521 ms after the reset, and the setup goes through.
The "wrong" file plays. Command 03 takes the global number — the
order in which files were written to the card, not the name. Copying a
whole folder does not promise any order. Choose the track with command
0F by folder and file.
The playlist jumps over a track. The clone sends "track finished" twice (16г.5). A guard interval after a track starts.
The playlist got stuck forever. Event 3D was corrupted by noise — and
there is nothing more to wait for. The backup path is BUSY: high while
playing for longer than 500 ms.
Not a single answer, yet the music plays. A clone without
acknowledgements 41 (16г.5) — or the answers arrive, but with a foreign
checksum. Look at the driver counters: "frames with a wrong sum" or
"timeouts without an answer".
Repeating a command jumps over a track. "Next" (01) was repeated.
Repeat only commands whose repetition is harmless: "folder/file", volume,
equalizer.
The track is not found, although the file is on the card. The name
does not follow the rule (01/001.mp3: a folder of two digits, a file of
three) — from memory of the documentation, not verified. The module
answers with error 6; the driver skips the item.
16г.8 Summary
- DFPlayer Mini is a decoder, a microSD slot and an amplifier; for the
microcontroller it is a UART at 9600 baud,
BRR= 7500 at 72 MHz, a frame of 10 bytes in 10.4 ms. - The frame:
7E FF 06 CMD FB PAR1 PAR2 CHK_H CHK_L EF, the checksum is zero minus the sum of the bytes fromFFto PAR2; "track 1" is7E FF 06 03 00 00 01 FE F7 EF. Command and event codes follow the documentation, not verified. - One command in flight: acknowledgement
41in 35…49 ms (model), no answer in 200 ms — a repeat, up to 3 attempts; error "busy" — a repeat after 250 ms. - The global track number is the write order of files: number 1 started
01/002. Choose a track by folder and file (0F). - Volume 0…30, the driver clips: 35 → 30.
- After power-up and reset wait for
3F: without it volume and equalizer were lost. - A clone with a double "track finished" played 4 of 6 without protection, with a guard interval of 1000 ms — 6 of 6.
- BUSY is a backup sign: by events the pause between tracks is 128…136 ms, by BUSY alone 598…633 ms; under noise BUSY found 5 lost events out of 100, the order is intact.
- The driver takes 356 bytes of RAM and about 2.9 KB of code on Cortex-M3.
Full sources
dfplayer.h — the driver: constants, command and event codes, state and prototypes.
/* dfplayer.h — раздел 16г: MP3-модуль DFPlayer Mini (YX5200 и клоны) по UART, 9600 бод.
* Кадр: 7E FF 06 CMD FB PAR1 PAR2 CHK_H CHK_L EF; CHK = 0 − (FF + 06 + CMD + FB + PAR1 + PAR2), 16 бит.
* dfplayer.c — кадр и сумма, разбор потока ответов без блокировки, одна команда в пути с подтверждением
* и повтором, очередь команд, плейлист «папка/файл», события модуля (трек закончился, карта, ошибка), BUSY.
* Коды команд и ответов — по документации производителя (в проекте её нет), не проверено.
* Платформа даёт одну функцию: df_hw_tx (байты в USART). Тот же код собирается в прошивку (fw16e.c)
* и в стенд на компьютере (host16e.c). */
#ifndef DFPLAYER_H
#define DFPLAYER_H
#include <stdint.h>
#define DF_RXQ 128u /* кольцо приёма */
#define DF_Q 8u /* очередь команд */
#define DF_LIST 32u /* плейлист */
#define DF_ACK_MS 200u /* срок ответа на команду */
#define DF_TRIES 3u /* попыток одной команды */
#define DF_BUSY_TRY 12u /* попыток, пока модуль отвечает «занят» (ошибка 1) */
#define DF_BUSY_WAIT 250u /* «занят» — повторить через столько мс */
#define DF_GAP_MS 100u /* модуль без подтверждений: пауза после команды */
#define DF_NOACK_N 3u /* столько сроков подряд, ни одного подтверждения — модуль «без ответов» */
#define DF_BADSUM_N 3u /* столько целых кадров подряд с чужой суммой, ни одного верного — сумму не проверять */
#define DF_BOOT_MS 3000u /* ждать 0x3F после включения или сброса */
#define DF_GUARD_MS 1000u /* после запуска трека: «трек закончился» — не про него, BUSY не смотреть */
#define DF_BUSY_MS 500u /* BUSY = 1 столько мс при игре — трек кончился, а событие пропало */
#define DF_VOL_MAX 30u
/* команды (микроконтроллер → модуль) */
enum {
DF_NEXT = 0x01, DF_PREV = 0x02, DF_TRACK = 0x03, DF_VOL = 0x06, DF_EQ = 0x07, DF_RESET = 0x0C,
DF_PLAY = 0x0D, DF_PAUSE = 0x0E, DF_FOLDER = 0x0F, DF_STOP = 0x16,
DF_Q_STATUS = 0x42, DF_Q_VOL = 0x43, DF_Q_EQ = 0x44, DF_Q_FILES = 0x48, DF_Q_TRACK = 0x4C, DF_Q_INFOLDER = 0x4E
};
/* ответы и события (модуль → микроконтроллер) */
enum { DF_R_IN = 0x3A, DF_R_OUT = 0x3B, DF_R_END_U = 0x3C, DF_R_END = 0x3D, DF_R_END_F = 0x3E, DF_R_ONLINE = 0x3F,
DF_R_ERR = 0x40, DF_R_ACK = 0x41 };
/* коды ошибки в ответе 0x40 */
enum { DF_E_BUSY = 1, DF_E_SLEEP = 2, DF_E_FRAME = 3, DF_E_SUM = 4, DF_E_RANGE = 5, DF_E_NOFILE = 6 };
enum { DF_ST_BOOT, DF_ST_READY, DF_ST_PLAY, DF_ST_PAUSE, DF_ST_NOCARD };
enum { DF_EV_STATE, DF_EV_READY, DF_EV_DONE, DF_EV_REPLY, DF_EV_END, DF_EV_STALE, DF_EV_ERR, DF_EV_CARD, DF_EV_MODE,
DF_EV_LIST_END };
enum { DF_END_EVENT = 1, DF_END_BUSY = 2, DF_END_SKIP = 3 }; /* чем замечен конец трека */
enum { DF_MODE_NOACK = 1, DF_MODE_LAX = 2 };
typedef struct df df_t;
typedef void (*df_event_fn)(df_t *d, uint8_t ev, uint16_t a, uint16_t b);
typedef struct {
uint8_t cmd;
uint16_t par;
} df_cmd_t;
typedef struct {
uint8_t folder, file;
} df_item_t;
struct df {
/* кольцо приёма: head пишет прерывание, tail — df_poll */
uint8_t rx[DF_RXQ];
volatile uint16_t rx_head;
uint16_t rx_tail, rx_hiwater;
volatile uint16_t rx_lost;
uint8_t f[10], fn; /* собираемый кадр */
/* команды */
df_cmd_t q[DF_Q];
uint8_t qh, qt;
df_cmd_t cur; /* команда в пути */
uint8_t busy, tries, btries;
uint32_t deadline, next_at;
uint8_t ack, lax; /* ack = 1 — просить подтверждение; lax = 1 — сумму принятых кадров не проверять */
uint8_t noack_run, badsum_run;
/* плейлист */
df_item_t list[DF_LIST];
uint8_t list_n, idx, loop, resume;
uint32_t play_at, busy_since;
uint8_t busy_hi;
/* состояние */
uint8_t st, vol, eq, dedup, boot_wait, use_events;
uint16_t files, online;
uint32_t now, boot_at;
df_event_fn on_event;
uint32_t n_tx, n_retry, n_ack, n_tmo, n_err, n_rx, n_good, n_badsum, n_junk, n_stale, n_end_ev, n_end_busy, n_skip;
};
/* платформа */
void df_hw_tx(const uint8_t *p, uint16_t n);
uint16_t df_sum(const uint8_t *f); /* сумма по байтам 1…6 кадра */
uint8_t df_frame(uint8_t cmd, uint8_t fb, uint16_t par, uint8_t *out); /* 10 байт */
void df_init(df_t *d, uint32_t now_ms, uint8_t vol, uint8_t eq, df_event_fn ev);
void df_rx_isr(df_t *d, uint8_t b);
void df_poll(df_t *d, uint32_t now_ms, uint8_t busy_pin); /* busy_pin: 0 — играет, 1 — тихо */
int df_cmd(df_t *d, uint8_t cmd, uint16_t par); /* 0 — в очереди, −1 — очередь полна */
int df_volume(df_t *d, uint8_t v); /* 0…30, больше — 30 */
int df_eq(df_t *d, uint8_t eq); /* 0…5 */
int df_play(df_t *d, const df_item_t *list, uint8_t n, uint8_t loop);
void df_pause(df_t *d);
void df_resume(df_t *d);
void df_stop(df_t *d);
void df_reset(df_t *d);
#endif
dfplayer.c — frame and checksum, reply parsing, command queue, playlist, events and BUSY.
/* dfplayer.c — раздел 16г: DFPlayer Mini по UART. Ни одного ожидания в цикле: df_poll разбирает то, что уже
* пришло, проверяет срок команды в пути, отправляет следующую из очереди и следит за BUSY.
* Коды команд, ответов и ошибок — по документации производителя (в проекте её нет), не проверено. */
#include <string.h>
#include "dfplayer.h"
static int after(uint32_t now, uint32_t t)
{
return (int32_t)(now - t) >= 0;
}
static int is_query(uint8_t c)
{
return c >= 0x42u && c <= 0x4Fu;
}
static void emit(df_t *d, uint8_t ev, uint16_t a, uint16_t b)
{
if (d->on_event)
d->on_event(d, ev, a, b);
}
static void set_st(df_t *d, uint8_t st)
{
if (d->st != st) {
d->st = st;
emit(d, DF_EV_STATE, st, 0);
}
}
/* ── кадр ────────────────────────────────────────────────────────────── */
uint16_t df_sum(const uint8_t *f)
{
uint16_t s = 0;
for (uint8_t i = 1; i < 7; i++) /* FF 06 CMD FB PAR1 PAR2 — без 7E, суммы и EF */
s = (uint16_t)(s + f[i]);
return (uint16_t)(0u - s);
}
uint8_t df_frame(uint8_t cmd, uint8_t fb, uint16_t par, uint8_t *out)
{
out[0] = 0x7E;
out[1] = 0xFF; /* версия */
out[2] = 0x06; /* длина: от версии до PAR2 */
out[3] = cmd;
out[4] = fb; /* 1 — прислать подтверждение 0x41 */
out[5] = (uint8_t)(par >> 8);
out[6] = (uint8_t)par;
uint16_t c = df_sum(out);
out[7] = (uint8_t)(c >> 8);
out[8] = (uint8_t)c;
out[9] = 0xEF;
return 10;
}
/* ── очередь команд: одна в пути ─────────────────────────────────────── */
int df_cmd(df_t *d, uint8_t cmd, uint16_t par)
{
uint8_t nx = (uint8_t)((d->qh + 1u) % DF_Q);
if (nx == d->qt)
return -1;
d->q[d->qh].cmd = cmd;
d->q[d->qh].par = par;
d->qh = nx;
return 0;
}
static void send_cur(df_t *d)
{
uint8_t w[10];
uint8_t q = (uint8_t)is_query(d->cur.cmd);
uint8_t fb = (uint8_t)(d->ack && !q);
df_hw_tx(w, df_frame(d->cur.cmd, fb, d->cur.par, w));
d->n_tx++;
d->busy = 1;
d->deadline = d->now + (fb || q ? DF_ACK_MS : DF_GAP_MS);
}
static void play_item(df_t *d)
{
const df_item_t *it = &d->list[d->idx];
df_cmd(d, DF_FOLDER, (uint16_t)(it->folder << 8 | it->file));
d->play_at = d->now;
d->busy_hi = 0;
set_st(d, DF_ST_PLAY);
}
/* конец трека: событием, по BUSY или пропуском (файла нет) — следующий пункт плейлиста */
static void finished(df_t *d, uint8_t how)
{
emit(d, DF_EV_END, d->idx, how);
if (++d->idx >= d->list_n) {
if (!d->loop) {
d->list_n = 0;
set_st(d, DF_ST_READY);
emit(d, DF_EV_LIST_END, 0, 0);
return;
}
d->idx = 0;
}
play_item(d);
}
static void done(df_t *d, uint8_t res) /* res: 0 — выполнена, иначе код ошибки, 0xFF — молчание */
{
uint8_t c = d->cur.cmd;
d->busy = 0;
emit(d, DF_EV_DONE, c, res);
if ((!res || res == 0xFF) && (c == DF_FOLDER || c == DF_PLAY))
d->play_at = d->now; /* трек пошёл (или пропал ответ — решит BUSY) */
if (res && res != 0xFF && c == DF_FOLDER && d->st == DF_ST_PLAY) { /* модуль сказал «нет файла» */
d->n_skip++;
finished(d, DF_END_SKIP);
}
}
static void retry(df_t *d)
{
if (d->tries >= DF_TRIES) {
done(d, 0xFF);
return;
}
d->tries++;
d->n_retry++;
send_cur(d);
}
static void on_timeout(df_t *d)
{
uint8_t q = (uint8_t)is_query(d->cur.cmd);
if (!q && !d->ack) {
done(d, 0); /* без подтверждений: пауза прошла — считаем выполненной */
return;
}
d->n_tmo++;
if (!q && !d->n_ack && ++d->noack_run >= DF_NOACK_N) {
d->ack = 0; /* ни одного 0x41 — клон без подтверждений */
emit(d, DF_EV_MODE, DF_MODE_NOACK, 0);
done(d, 0); /* ушла DF_NOACK_N раз: «папка/файл» и громкость повторять безвредно */
return;
}
retry(d);
}
static void on_error(df_t *d, uint8_t code)
{
d->n_err++;
if (d->busy != 1) {
emit(d, DF_EV_ERR, code, 0); /* ошибка без команды в пути */
return;
}
if (code == DF_E_FRAME || code == DF_E_SUM) {
retry(d); /* модуль принял кадр испорченным */
} else if (code == DF_E_BUSY && d->btries < DF_BUSY_TRY) {
d->btries++;
d->busy = 2; /* занят (читает карту) — отправить снова позже */
d->deadline = d->now + DF_BUSY_WAIT;
} else {
done(d, code);
}
}
static void ready(df_t *d)
{
emit(d, DF_EV_READY, d->online, (uint16_t)(d->now - d->boot_at));
set_st(d, DF_ST_READY);
df_volume(d, d->vol);
df_eq(d, d->eq);
df_cmd(d, DF_Q_FILES, 0);
if (d->resume && d->list_n)
play_item(d);
d->resume = 0;
}
static void track_end(df_t *d, uint16_t par)
{
if (!d->use_events)
return;
if (d->st != DF_ST_PLAY || (d->dedup && !after(d->now, d->play_at + DF_GUARD_MS))) {
d->n_stale++; /* второй раз о том же или о прежнем треке */
emit(d, DF_EV_STALE, par, 0);
return;
}
d->n_end_ev++;
finished(d, DF_END_EVENT);
}
static void on_frame(df_t *d, uint8_t cmd, uint16_t par)
{
switch (cmd) {
case DF_R_ACK:
d->n_ack++;
d->noack_run = 0;
if (d->busy == 1 && !is_query(d->cur.cmd))
done(d, 0);
return;
case DF_R_ERR:
on_error(d, (uint8_t)par);
return;
case DF_R_ONLINE: /* модуль готов; PAR2 — носители: 2 — карта */
d->online = par;
if (d->st == DF_ST_BOOT)
ready(d);
return;
case DF_R_OUT:
emit(d, DF_EV_CARD, 0, par);
if (d->st == DF_ST_PLAY)
d->resume = 1;
d->qh = d->qt = 0;
d->busy = 0;
set_st(d, DF_ST_NOCARD);
return;
case DF_R_IN:
emit(d, DF_EV_CARD, 1, par);
if (d->st != DF_ST_NOCARD)
return;
set_st(d, DF_ST_READY);
if (d->resume && d->list_n)
play_item(d); /* тот же пункт сначала; пока карта читается — «занят» */
d->resume = 0;
return;
case DF_R_END_U:
case DF_R_END:
case DF_R_END_F:
track_end(d, par);
return;
default:
if (is_query(cmd) && d->busy == 1 && d->cur.cmd == cmd) {
if (cmd == DF_Q_FILES)
d->files = par;
emit(d, DF_EV_REPLY, cmd, par);
done(d, 0);
} else {
d->n_junk++; /* ответ не на нашу команду */
}
}
}
/* кадр собирается по байту; не тот вид — ищем следующий 7E внутри собранного */
static void rx_byte(df_t *d, uint8_t b)
{
if (d->fn == 0 && b != 0x7E) {
d->n_junk++;
return;
}
d->f[d->fn++] = b;
if (d->fn < 10)
return;
d->fn = 0;
if (d->f[1] != 0xFF || d->f[2] != 0x06 || d->f[9] != 0xEF) {
d->n_junk++;
for (uint8_t i = 1; i < 10; i++)
if (d->f[i] == 0x7E) {
memmove(d->f, d->f + i, (size_t)(10u - i));
d->fn = (uint8_t)(10u - i);
break;
}
return;
}
if ((uint16_t)(d->f[7] << 8 | d->f[8]) != df_sum(d->f)) {
d->n_badsum++;
if (!d->lax && !d->n_good && ++d->badsum_run >= DF_BADSUM_N) {
d->lax = 1; /* целые кадры, ни одной верной суммы — клон считает иначе */
emit(d, DF_EV_MODE, DF_MODE_LAX, 0);
}
if (!d->lax)
return;
} else {
d->n_good++;
d->badsum_run = 0;
}
d->n_rx++;
on_frame(d, d->f[3], (uint16_t)(d->f[5] << 8 | d->f[6]));
}
/* ── общий вход ──────────────────────────────────────────────────────── */
int df_volume(df_t *d, uint8_t v)
{
if (v > DF_VOL_MAX)
v = DF_VOL_MAX;
d->vol = v;
return df_cmd(d, DF_VOL, v);
}
int df_eq(df_t *d, uint8_t eq)
{
if (eq > 5u)
return -1;
d->eq = eq;
return df_cmd(d, DF_EQ, eq);
}
int df_play(df_t *d, const df_item_t *list, uint8_t n, uint8_t loop)
{
if (!n || n > DF_LIST)
return -1;
memcpy(d->list, list, n * sizeof *list);
d->list_n = n;
d->idx = 0;
d->loop = loop;
if (d->st == DF_ST_BOOT || d->st == DF_ST_NOCARD)
d->resume = 1; /* начнётся, когда модуль будет готов */
else
play_item(d);
return 0;
}
void df_pause(df_t *d)
{
if (d->st != DF_ST_PLAY)
return;
df_cmd(d, DF_PAUSE, 0);
set_st(d, DF_ST_PAUSE);
}
void df_resume(df_t *d)
{
if (d->st != DF_ST_PAUSE)
return;
df_cmd(d, DF_PLAY, 0);
d->play_at = d->now;
d->busy_hi = 0;
set_st(d, DF_ST_PLAY);
}
void df_stop(df_t *d)
{
df_cmd(d, DF_STOP, 0);
d->list_n = 0;
set_st(d, DF_ST_READY);
}
void df_reset(df_t *d)
{
uint8_t w[10];
d->resume = (uint8_t)(d->st == DF_ST_PLAY);
d->qh = d->qt = 0;
d->busy = 0;
df_hw_tx(w, df_frame(DF_RESET, 0, 0, w)); /* подтверждения не ждём: модуль перезагружается */
d->n_tx++;
d->boot_at = d->now;
set_st(d, DF_ST_BOOT);
}
void df_init(df_t *d, uint32_t now, uint8_t vol, uint8_t eq, df_event_fn ev)
{
memset(d, 0, sizeof *d);
d->now = d->boot_at = now;
d->vol = vol > DF_VOL_MAX ? DF_VOL_MAX : vol;
d->eq = eq;
d->ack = d->dedup = d->boot_wait = d->use_events = 1;
d->on_event = ev;
d->st = DF_ST_BOOT;
}
void df_rx_isr(df_t *d, uint8_t b)
{
uint16_t nx = (uint16_t)((d->rx_head + 1u) % DF_RXQ);
if (nx == d->rx_tail) {
d->rx_lost++;
return;
}
d->rx[d->rx_head] = b;
d->rx_head = nx;
}
void df_poll(df_t *d, uint32_t now, uint8_t busy_pin)
{
d->now = now;
uint16_t used = (uint16_t)((d->rx_head - d->rx_tail + DF_RXQ) % DF_RXQ);
if (used > d->rx_hiwater)
d->rx_hiwater = used;
while (d->rx_tail != d->rx_head) {
uint8_t b = d->rx[d->rx_tail];
d->rx_tail = (uint16_t)((d->rx_tail + 1u) % DF_RXQ);
rx_byte(d, b);
}
if (d->st == DF_ST_BOOT && (!d->boot_wait || after(now, d->boot_at + DF_BOOT_MS)))
ready(d); /* 0x3F так и не пришёл — клон молчит при включении */
if (d->busy == 1 && after(now, d->deadline))
on_timeout(d);
else if (d->busy == 2 && after(now, d->deadline))
send_cur(d);
if (!d->busy && d->st != DF_ST_BOOT && d->qt != d->qh) {
d->cur = d->q[d->qt];
d->qt = (uint8_t)((d->qt + 1u) % DF_Q);
d->tries = 1;
d->btries = 0;
send_cur(d);
}
/* BUSY: 0 — играет. При игре 1 дольше DF_BUSY_MS — трек кончился, а событие пропало */
if (d->st == DF_ST_PLAY && busy_pin && after(now, d->play_at + DF_GUARD_MS)) {
if (!d->busy_hi) {
d->busy_hi = 1;
d->busy_since = now;
} else if (after(now, d->busy_since + DF_BUSY_MS)) {
d->n_end_busy++;
finished(d, DF_END_BUSY);
}
} else {
d->busy_hi = 0;
}
}
fw16e.c — STM32F103 firmware: USART1 at 9600 baud with interrupts, BUSY, a pause button, the main loop.
/* fw16e.c — раздел 16г: DFPlayer Mini на STM32F103 (BluePill), 72 МГц.
* USART1, 9600 бод: PA9 — TX через резистор 1 кОм к RX модуля, PA10 — RX от TX модуля.
* PB5 — вход BUSY модуля с подтяжкой к питанию (0 — играет). PA0 — кнопка к земле: пауза и продолжение.
* PC13 — светодиод: горит, пока трек играет. Приём — прерывание RXNE кладёт байт в кольцо драйвера;
* передача — своё кольцо и прерывание TXE. SysTick раз в 1 мс — только счёт времени; df_poll — в главном
* цикле. Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); USART1 на APB2 = 72 МГц. */
#include "stm32f10x.h"
#include "dfplayer.h"
#define CPU_HZ 72000000u
#define BAUD 9600u
#define TXQ 64u
_Static_assert(CPU_HZ / BAUD == 7500u && CPU_HZ % BAUD == 0u, "USART1: BRR = 7500 — 9600 бод");
static df_t D;
static volatile uint32_t ms;
static uint8_t txq[TXQ];
static volatile uint16_t tx_head, tx_tail;
static uint32_t key_at;
static uint8_t key_last = 1;
static const df_item_t LIST[] = {{1, 1}, {1, 2}, {1, 3}, {2, 1}};
void SysTick_Handler(void)
{
ms++;
}
void USART1_IRQHandler(void)
{
uint32_t sr = USART1->SR;
if (sr & (USART_SR_RXNE | USART_SR_ORE)) /* чтение DR снимает и RXNE, и ORE */
df_rx_isr(&D, (uint8_t)USART1->DR);
if ((USART1->CR1 & USART_CR1_TXEIE) && (sr & USART_SR_TXE)) {
if (tx_tail != tx_head) {
USART1->DR = txq[tx_tail];
tx_tail = (uint16_t)((tx_tail + 1u) % TXQ);
} else {
USART1->CR1 &= ~USART_CR1_TXEIE; /* очередь пуста */
}
}
}
/* ── платформа драйвера ───────────────────────────────────────────────── */
void df_hw_tx(const uint8_t *p, uint16_t n)
{
for (uint16_t i = 0; i < n; i++) {
uint16_t nx = (uint16_t)((tx_head + 1u) % TXQ);
if (nx == tx_tail)
break; /* в пути не больше одного кадра — 10 байт */
txq[tx_head] = p[i];
tx_head = nx;
}
USART1->CR1 |= USART_CR1_TXEIE;
}
/* ── приложение ───────────────────────────────────────────────────────── */
static void on_event(df_t *d, uint8_t ev, uint16_t a, uint16_t b)
{
(void)d;
(void)b;
if (ev == DF_EV_STATE)
GPIOC->BSRR = a == DF_ST_PLAY ? 1u << 29 : 1u << 13; /* PC13 = 0 — светодиод горит */
}
static void board_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_USART1EN;
GPIOB->BSRR = 1u << 5; /* PB5 — подтяжка к питанию */
GPIOB->CRL = (GPIOB->CRL & ~(0xFu << 20)) | 0x8u << 20; /* PB5 — вход с подтяжкой */
GPIOA->BSRR = 1u << 0; /* PA0 — подтяжка к питанию */
GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x8u; /* PA0 — вход с подтяжкой */
GPIOA->CRH = (GPIOA->CRH & ~0xFF0u)
| 0xBu << 4 /* PA9 — USART1_TX, альтернативная функция */
| 0x4u << 8; /* PA10 — USART1_RX, вход без подтяжки */
GPIOC->BSRR = 1u << 13;
GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | 0x2u << 20; /* PC13 — светодиод платы */
USART1->BRR = CPU_HZ / BAUD;
USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE;
NVIC_EnableIRQ(USART1_IRQn);
SysTick_Config(CPU_HZ / 1000u);
}
int main(void)
{
board_init();
df_init(&D, ms, 20, 0, on_event);
df_play(&D, LIST, sizeof LIST / sizeof LIST[0], 1); /* начнётся после 0x3F */
for (;;) {
uint32_t now = ms;
df_poll(&D, now, (uint8_t)(GPIOB->IDR >> 5 & 1u));
uint8_t key = (uint8_t)(GPIOA->IDR & 1u);
if (key != key_last && now - key_at >= 20u) { /* дребезг: 20 мс после прошлой смены */
key_last = key;
key_at = now;
if (!key) {
if (D.st == DF_ST_PLAY)
df_pause(&D);
else
df_resume(&D);
}
}
__WFI(); /* до следующего прерывания: байта или тика */
}
}
Next: section 17 "The I²C bus" — two wires, many devices and the most temperamental peripheral of them all.