mirror of
https://github.com/RIOT-OS/RIOT.git
synced 2024-12-29 04:50:03 +01:00
0bc08dfbbc
The error code was previously overwritten :-/ this clearly is not how it is intended to work.
457 lines
14 KiB
C
457 lines
14 KiB
C
/*
|
|
* Copyright 2019 Marian Buschsieweke
|
|
*
|
|
* This file is subject to the terms and conditions of the GNU Lesser
|
|
* General Public License v2.1. See the file LICENSE in the top level
|
|
* directory for more details.
|
|
*/
|
|
|
|
/**
|
|
* @ingroup drivers_dfplayer
|
|
* @{
|
|
*
|
|
* @file
|
|
* @brief Implementation DFPlayer Mini Device Driver
|
|
* @author Marian Buschsieweke <marian.buschsieweke@ovgu.de>
|
|
*
|
|
* @}
|
|
*/
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <inttypes.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
#include "dfplayer.h"
|
|
#include "dfplayer_constants.h"
|
|
#include "dfplayer_internal.h"
|
|
#include "periph/gpio.h"
|
|
#include "periph/uart.h"
|
|
#include "thread.h"
|
|
#include "xtimer.h"
|
|
|
|
#define ENABLE_DEBUG 0
|
|
#include "debug.h"
|
|
|
|
/**
|
|
* @brief Initial value of the frame check sequence
|
|
*/
|
|
static const uint16_t fcs_init = -(DFPLAYER_VERSION + DFPLAYER_LEN);
|
|
|
|
/**
|
|
* @brief Identify the source from an insert/eject event
|
|
*
|
|
* @param dev DFPlayer device descriptor
|
|
*
|
|
* @return The source that was inserted / ejected
|
|
* @retval DFPLAYER_SOURCE_NUMOF Unknown source
|
|
*/
|
|
static dfplayer_source_t _get_inserted_ejected_source(dfplayer_t *dev)
|
|
{
|
|
switch (dev->buf[3]) {
|
|
case DFPLAYER_DEVICE_USB:
|
|
DEBUG("[dfplayer] Inserted/ejected USB storage device\n");
|
|
return DFPLAYER_SOURCE_USB;
|
|
case DFPLAYER_DEVICE_SDCARD:
|
|
DEBUG("[dfplayer] Inserted/ejected SD card\n");
|
|
return DFPLAYER_SOURCE_SDCARD;
|
|
}
|
|
|
|
DEBUG("[dfplayer] Insert/eject event with unknown source\n");
|
|
return DFPLAYER_SOURCE_NUMOF;
|
|
}
|
|
|
|
/**
|
|
* @brief Handle a playback completed event
|
|
*
|
|
* @param dev DFPlayer device descriptor
|
|
* @param src Medium the track was played from
|
|
*/
|
|
static void _handle_playback_completed(dfplayer_t *dev, dfplayer_source_t src)
|
|
{
|
|
uint16_t track = (((uint16_t)dev->buf[2]) << 8) | dev->buf[3];
|
|
DEBUG("[dfplayer] Playback of track %" PRIu16 " on medium %u completed\n",
|
|
track, (unsigned)src);
|
|
|
|
dev->flags |= DFPLAYER_FLAG_NO_ACK_BUG;
|
|
|
|
/* Note: At least some revisions report playback completed more than once,
|
|
* maybe to increase probability of the message reaching the MCU. This
|
|
* de-duplicates the message by ignoring follow up messages for 100ms.
|
|
* Filtering by track number and medium wouldn't work here, as the same
|
|
* song might be played in repeat mode.
|
|
*/
|
|
uint32_t now_us = xtimer_now_usec();
|
|
if (dev->cb_done && (now_us - dev->last_event_us > DFPLAYER_TIMEOUT_MS * US_PER_MS)) {
|
|
dev->cb_done(dev, src, track);
|
|
}
|
|
dev->last_event_us = now_us;
|
|
}
|
|
|
|
/**
|
|
* @brief Parse the bootup completed frame and init available sources
|
|
*
|
|
* @param dev DFPlayer device descriptor
|
|
*/
|
|
static void _handle_bootup_completed(dfplayer_t *dev)
|
|
{
|
|
if (dev->buf[3] & DFPLAYER_MASK_USB) {
|
|
dev->srcs |= 0x01 << DFPLAYER_SOURCE_USB;
|
|
}
|
|
|
|
if (dev->buf[3] & DFPLAYER_MASK_SDCARD) {
|
|
dev->srcs |= 0x01 << DFPLAYER_SOURCE_SDCARD;
|
|
}
|
|
|
|
if (dev->buf[3] & DFPLAYER_MASK_FLASH) {
|
|
dev->srcs |= 0x01 << DFPLAYER_SOURCE_FLASH;
|
|
}
|
|
|
|
/* Unblock caller of dfplayer_reset() */
|
|
mutex_unlock(&dev->sync);
|
|
}
|
|
|
|
/**
|
|
* @brief Handle a notification message
|
|
*/
|
|
static void _handle_event_notification(dfplayer_t *dev)
|
|
{
|
|
switch (dev->buf[0]) {
|
|
case DFPLAYER_NOTIFY_INSERT:
|
|
DEBUG("[dfplayer] Insert event\n");
|
|
{
|
|
dfplayer_source_t src = _get_inserted_ejected_source(dev);
|
|
if (src < DFPLAYER_SOURCE_NUMOF) {
|
|
dev->srcs |= (dfplayer_source_set_t)(0x01 << src);
|
|
}
|
|
}
|
|
if (dev->cb_src) {
|
|
dev->cb_src(dev, dev->srcs);
|
|
}
|
|
return;
|
|
case DFPLAYER_NOTIFY_EJECT:
|
|
DEBUG("[dfplayer] Eject event\n");
|
|
{
|
|
dfplayer_source_t src = _get_inserted_ejected_source(dev);
|
|
if (src < DFPLAYER_SOURCE_NUMOF) {
|
|
dev->srcs &= ~((dfplayer_source_set_t)(0x01 << src));
|
|
}
|
|
}
|
|
if (dev->cb_src) {
|
|
dev->cb_src(dev, dev->srcs);
|
|
}
|
|
return;
|
|
case DFPLAYER_NOTIFY_DONE_USB:
|
|
_handle_playback_completed(dev, DFPLAYER_SOURCE_USB);
|
|
return;
|
|
case DFPLAYER_NOTIFY_DONE_SDCARD:
|
|
_handle_playback_completed(dev, DFPLAYER_SOURCE_SDCARD);
|
|
return;
|
|
case DFPLAYER_NOTIFY_DONE_FLASH:
|
|
_handle_playback_completed(dev, DFPLAYER_SOURCE_FLASH);
|
|
return;
|
|
case DFPLAYER_NOTIFY_READY:
|
|
_handle_bootup_completed(dev);
|
|
return;
|
|
default:
|
|
DEBUG("[dfplayer] Unknown notification (%02x)\n", dev->buf[0]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Parse the frame received from the DFPlayer Mini
|
|
*
|
|
* @param dev Device descriptor of the DFPlayer the frame received from
|
|
*
|
|
* The frame is stored in the buffer of the device descriptor
|
|
*/
|
|
static void _parse_frame(dfplayer_t *dev)
|
|
{
|
|
assert(dev->len == DFPLAYER_LEN);
|
|
switch (dev->buf[0] & DFPLAYER_CLASS_MASK) {
|
|
case DFPLAYER_CLASS_NOTIFY:
|
|
_handle_event_notification(dev);
|
|
return;
|
|
case DFPLAYER_CLASS_RESPONSE:
|
|
/* Unblock thread waiting for response */
|
|
mutex_unlock(&dev->sync);
|
|
return;
|
|
}
|
|
|
|
DEBUG("[dfplayer] Got frame of unknown class\n");
|
|
}
|
|
|
|
/**
|
|
* @brief Function called when a byte was received over UART (ISR-context)
|
|
*
|
|
* @param _dev The corresponding device descriptor
|
|
* @param data The received byte of data
|
|
*/
|
|
void dfplayer_uart_rx_cb(void *_dev, uint8_t data)
|
|
{
|
|
dfplayer_t *dev = _dev;
|
|
switch (dev->state) {
|
|
case DFPLAYER_RX_STATE_START:
|
|
if (data == DFPLAYER_START) {
|
|
dev->state = DFPLAYER_RX_STATE_VERSION;
|
|
return;
|
|
}
|
|
break;
|
|
case DFPLAYER_RX_STATE_VERSION:
|
|
if (data == DFPLAYER_VERSION) {
|
|
dev->state = DFPLAYER_RX_STATE_LENGTH;
|
|
return;
|
|
}
|
|
break;
|
|
case DFPLAYER_RX_STATE_LENGTH:
|
|
if (data == DFPLAYER_LEN) {
|
|
dev->len = 0;
|
|
dev->state = DFPLAYER_RX_STATE_DATA;
|
|
return;
|
|
}
|
|
else {
|
|
DEBUG("[dfplayer] Got frame with length %" PRIu8 ", but all "
|
|
"frames should have length 6\n", data);
|
|
}
|
|
break;
|
|
case DFPLAYER_RX_STATE_DATA:
|
|
/* We are a bit more liberal here and allow the end symbol to
|
|
* appear in the payload of the frame, as the data sheet does not
|
|
* mention any sort of escaping to prevent it from appearing in the
|
|
* frame's payload. If bytes get lost and an and of frame symbol
|
|
* is mistaken for a payload byte, this will be almost certainly
|
|
* detected, as additionally a second end of frame symbol would
|
|
* need to appear at the right position *and* the frame check
|
|
* sequence need to match
|
|
*/
|
|
if ((data == DFPLAYER_END) && (dev->len == DFPLAYER_LEN)) {
|
|
uint16_t fcs_exp = fcs_init;
|
|
fcs_exp -= dev->buf[0] + dev->buf[1] + dev->buf[2] + dev->buf[3];
|
|
uint16_t fcs = (((uint16_t)dev->buf[4]) << 8) | dev->buf[5];
|
|
if (fcs == fcs_exp) {
|
|
DEBUG("[dfplayer] Got 0x%02x, 0x%02x, 0x%02x, 0x%02x\n",
|
|
dev->buf[0], dev->buf[1], dev->buf[2],
|
|
dev->buf[3]);
|
|
_parse_frame(dev);
|
|
}
|
|
else {
|
|
DEBUG("[dfplayer] Checksum mismatch");
|
|
}
|
|
}
|
|
else if (dev->len < sizeof(dev->buf)) {
|
|
dev->buf[dev->len++] = data;
|
|
return;
|
|
}
|
|
else {
|
|
DEBUG("[dfplayer] Frame overflown\n");
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
dev->state = DFPLAYER_RX_STATE_START;
|
|
}
|
|
|
|
static int _send(dfplayer_t *dev, uint8_t cmd, uint8_t p1, uint8_t p2,
|
|
uint32_t timeout_us)
|
|
{
|
|
int retval;
|
|
if (dev->flags & DFPLAYER_FLAG_NO_ACK_BUG) {
|
|
/* Hardware bug: The next command will not be ack'ed, unless it is
|
|
* a query command. We can clear the flag, as we issue now a fake query,
|
|
* if needed.
|
|
*/
|
|
dev->flags &= ~(DFPLAYER_FLAG_NO_ACK_BUG);
|
|
if (cmd < DFPLAYER_LOWEST_QUERY) {
|
|
/* Command is a control command, we query the volume and ignore the
|
|
* result as work around */
|
|
retval = _send(dev, DFPLAYER_CMD_GET_VOLUME, 0, 0,
|
|
DFPLAYER_TIMEOUT_MS * US_PER_MS);
|
|
if (retval) {
|
|
/* pass through error */
|
|
return retval;
|
|
}
|
|
}
|
|
}
|
|
|
|
uint16_t fcs = fcs_init - (cmd + DFPLAYER_ACK + p1 + p2);
|
|
uint8_t frame[] = {
|
|
DFPLAYER_START, DFPLAYER_VERSION, DFPLAYER_LEN, cmd, DFPLAYER_ACK,
|
|
p1, p2, (uint8_t)(fcs>>8), (uint8_t)fcs, DFPLAYER_END
|
|
};
|
|
|
|
for (unsigned i = 0; i < DFPLAYER_RETRIES; i++) {
|
|
retval = 0;
|
|
DEBUG("[dfplayer] About to exchange frame\n");
|
|
/* Enforce that mutex is locked, so that xtimer_mutex_lock_timeout()
|
|
* will not return immediately. */
|
|
mutex_trylock(&dev->sync);
|
|
uart_write(dev->uart, frame, sizeof(frame));
|
|
|
|
if (xtimer_mutex_lock_timeout(&dev->sync, timeout_us)) {
|
|
DEBUG("[dfplayer] Response timed out\n");
|
|
retval = -ETIMEDOUT;
|
|
}
|
|
else {
|
|
uint8_t code = dev->buf[0];
|
|
if (code == DFPLAYER_RESPONSE_ERROR) {
|
|
switch (dev->buf[3]) {
|
|
case DFPLAYER_ERROR_BUSY:
|
|
DEBUG("[dfplayer] Error: Module is busy\n");
|
|
retval = -EAGAIN;
|
|
break;
|
|
case DFPLAYER_ERROR_FRAME:
|
|
DEBUG("[dfplayer] Error: DFPlayer received incomplete "
|
|
"frame\n");
|
|
retval = -EIO;
|
|
break;
|
|
case DFPLAYER_ERROR_FCS:
|
|
DEBUG("[dfplayer] Error: DFPlayer received corrupt frame "
|
|
"(FCS mismatch)\n");
|
|
retval = -EIO;
|
|
break;
|
|
default:
|
|
DEBUG("[dfplayer] Unknown error!\n");
|
|
/* This should never be reached according the datasheet */
|
|
retval = -EIO;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* wait to work around HW bug */
|
|
xtimer_msleep(DFPLAYER_SEND_DELAY_MS);
|
|
|
|
if (!retval) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
int dfplayer_transceive(dfplayer_t *dev, uint16_t *resp,
|
|
uint8_t cmd, uint8_t p1, uint8_t p2)
|
|
{
|
|
if (!dev) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
mutex_lock(&dev->mutex);
|
|
|
|
int retval = _send(dev, cmd, p1, p2, DFPLAYER_TIMEOUT_MS * US_PER_MS);
|
|
if (retval) {
|
|
mutex_unlock(&dev->mutex);
|
|
return retval;
|
|
}
|
|
|
|
if (resp) {
|
|
*resp = (((uint16_t)dev->buf[2]) << 8) | (uint16_t)dev->buf[3];
|
|
}
|
|
|
|
mutex_unlock(&dev->mutex);
|
|
return 0;
|
|
}
|
|
|
|
int dfplayer_reset(dfplayer_t *dev)
|
|
{
|
|
if (!dev) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
mutex_lock(&dev->mutex);
|
|
|
|
int retval = _send(dev, DFPLAYER_CMD_RESET, 0, 0,
|
|
DFPLAYER_TIMEOUT_MS * US_PER_MS);
|
|
|
|
if (retval) {
|
|
mutex_unlock(&dev->mutex);
|
|
return retval;
|
|
}
|
|
|
|
/* Enforce that mutex is locked, so that xtimer_mutex_lock_timeout()
|
|
* will not return immediately. */
|
|
mutex_trylock(&dev->sync);
|
|
|
|
const uint32_t bootup_timeout = DFPLAYER_BOOTUP_TIME_MS * US_PER_MS;
|
|
if (xtimer_mutex_lock_timeout(&dev->sync, bootup_timeout)) {
|
|
mutex_unlock(&dev->mutex);
|
|
DEBUG("[dfplayer] Waiting for device to boot timed out\n");
|
|
return -ETIMEDOUT;
|
|
}
|
|
|
|
uint8_t code = dev->buf[0];
|
|
mutex_unlock(&dev->mutex);
|
|
|
|
if (code != DFPLAYER_NOTIFY_READY) {
|
|
DEBUG("[dfplayer] Got unexpected response after reset\n");
|
|
return -EIO;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int dfplayer_file_cmd(dfplayer_t *dev, uint8_t cmd, uint8_t p1, uint8_t p2)
|
|
{
|
|
int retval = _send(dev, cmd, p1, p2, DFPLAYER_TIMEOUT_MS * US_PER_MS);
|
|
if (retval) {
|
|
return retval;
|
|
}
|
|
|
|
/* Enforce that mutex is locked, so that xtimer_mutex_lock_timeout()
|
|
* will not return immediately. */
|
|
mutex_trylock(&dev->sync);
|
|
|
|
const uint32_t timeout_us = DFPLAYER_TIMEOUT_MS * US_PER_MS;
|
|
if (xtimer_mutex_lock_timeout(&dev->sync, timeout_us)) {
|
|
/* For commands DFPLAYER_CMD_PLAY_FROM_MP3 (0x12) and
|
|
* DFPLAYER_CMD_PLAY_ADVERT (0x13) a second reply is only generated on
|
|
* failure. A timeout could be either:
|
|
* a) Success. DFPlayer is playing the selected file
|
|
* or
|
|
* b) Failure, but the reply got lost (or was rejected due to mismatch
|
|
* of the frame check sequence)
|
|
*
|
|
* We just check if the DFPlayer is actually playing
|
|
*/
|
|
if (gpio_is_valid(dev->busy_pin)) {
|
|
retval = 0;
|
|
/* Using BUSY pin to check if device is playing */
|
|
if (gpio_read(dev->busy_pin)) {
|
|
/* Device not playing, file does not exist */
|
|
retval = -ENOENT;
|
|
}
|
|
}
|
|
else {
|
|
/* BUSY pin not connected, query status instead */
|
|
retval = _send(dev, DFPLAYER_CMD_GET_STATUS, 0, 0, timeout_us);
|
|
|
|
if (!retval) {
|
|
uint8_t status = dev->buf[3];
|
|
retval = (status & DFPLAYER_STATUS_PLAYING) ? 0 : -ENOENT;
|
|
}
|
|
}
|
|
}
|
|
uint8_t code = dev->buf[0];
|
|
uint8_t error = dev->buf[3];
|
|
|
|
if (retval) {
|
|
return retval;
|
|
}
|
|
|
|
if (code == DFPLAYER_RESPONSE_ERROR) {
|
|
/* The DFPlayer already acknowledged successful reception of the
|
|
* command, so we expect that the only cause for an error is that the
|
|
* file was not found. But better check anyway, the device is strange */
|
|
if (error == DFPLAYER_ERROR_NO_SUCH_FILE) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
DEBUG("[dfplayer] Got unexpected error message\n");
|
|
return -EIO;
|
|
}
|
|
|
|
return 0;
|
|
}
|