Compare commits

...

22 Commits

Author SHA1 Message Date
King Kévin 9b8632c893 app: ensure track is not repeated 2021-06-24 21:49:57 +02:00
King Kévin ae875daba7 app: initialiaze random number generator 2021-06-24 21:49:32 +02:00
King Kévin 630945b27c app: display when error with SD card 2021-06-24 20:30:55 +02:00
King Kévin bc06b38260 app: display bluetooth status/search 2021-06-24 20:26:34 +02:00
King Kévin dc822fd341 README: document two new secret modes 2021-04-23 15:29:16 +02:00
King Kévin 80d5a2b704 application: make door open/close sequence more generic/clean 2021-04-23 15:21:11 +02:00
King Kévin 2758b5d4c4 application: add secret techno and kanguru modes 2021-04-23 14:59:41 +02:00
King Kévin 51f78852e5 application: add talk and exit track 2021-04-23 14:57:35 +02:00
King Kévin f81ec06f93 application: define at one location the folder for the different track types 2021-04-22 21:28:24 +02:00
King Kévin 08134e0d88 application: add multiple welcome tracks 2021-04-22 21:21:26 +02:00
King Kévin a22de00ee8 application: add 2 new talk tracks 2021-04-22 21:20:37 +02:00
King Kévin cac967ba76 application: minor, update copyright date 2021-04-22 21:18:56 +02:00
King Kévin 557c8777b1 application: new way on playing songs
now always plays right after the end of a song/message instead of waiting fixed times.
announces the actual time instead of waiting for the 3 minutes mark.
plays songs and comical messages in an endless loop.
plays a message when the door is opened.
2020-11-06 10:01:06 +01:00
King Kévin a100716cfc remove unused libraries 2020-10-11 12:48:31 +02:00
King Kévin fcafec7cc6 document project 2020-10-11 12:46:53 +02:00
King Kévin 804ba7628b application: allows BT audio pairing, play random songs depending on time 2020-09-27 12:00:22 +02:00
King Kévin f72e790a95 led_sk6812rgbw: extended version of WS2812B library, supporting 4th color, using bit banding, removing need of timer 2020-09-27 11:57:46 +02:00
King Kévin b20c272c8e led_tm1637: allow the display to be upside down 2020-09-27 11:56:31 +02:00
King Kévin 7dcfa18d8d README: draft, add pinout 2020-08-20 13:58:27 +02:00
King Kévin f58d7c6da4 application: first test for pinkeluhr firmware 2020-08-20 13:58:23 +02:00
King Kévin 69402a5525 led_ws2812b: minorx, fix comment 2020-08-20 13:56:11 +02:00
King Kévin bd631f6c12 led_tm1637: fix updating (both command and data need to be sent every time 2020-08-20 13:56:11 +02:00
54 changed files with 896 additions and 9616 deletions

View File

@ -1,55 +1,80 @@
This firmware template is designed for development boards based around [STM32 F1 series micro-controller](http://www.st.com/web/en/catalog/mmc/FM141/SC1169/SS1031).
This is the firmware for the Dachboden Klo-Assistant.
project
=======
summary
-------
This project's original purpose was to indicate the occupancy state of the toilet.
If the toilet was in use, a red light on a shield would indicate it is occupied.
Else a green light indicates it is free.
*describe project purpose*
Next, a display has been added to show the time spent in the water room.
This would put some pressure on the user to not spend too much time on the loo and hold up the queue.
technology
----------
Finally, it has been connected to speakers.
A welcome message would greed the new comer, followed by a (random) pleasant music.
Then the time passed inside is announced, followed by a (random) comical message.
And the loop continues with another song.
Once you exit the place, a short (random) message is played.
*described electronic details*
Two secret modes have been added:
- if the door is closed, opened, and closed, each step within 2 seconds, then techno music is continuously played (replacing all toilet messages)
- if the door is further opened and closed, sketches from the Känguru-Chroniken by Marc-Uwe Kling are played
board
=====
The current implementation uses a [core board](https://wiki.cuvoodoo.info/doku.php?id=stm32f1xx#core_board).
The devices is comprised by following components:
The underlying template also supports following board:
- [Maple Mini](http://leaflabs.com/docs/hardware/maple-mini.html), based on a STM32F103CBT6
- [System Board](https://wiki.cuvoodoo.info/doku.php?id=stm32f1xx#system_board), based on a STM32F103C8T6
- [blue pill](https://wiki.cuvoodoo.info/doku.php?id=stm32f1xx#blue_pill), based on a STM32F103C8T6
- [black pill](https://wiki.cuvoodoo.info/doku.php?id=stm32f1xx#black_pill), based on a STM32F103C8T6
- [core board](https://wiki.cuvoodoo.info/doku.php?id=stm32f1xx#core_board), based on a STM32F103C8T6
- [ST-LINK V2 mini](https://wiki.cuvoodoo.info/doku.php?id=jtag#mini_st-link_v2), a ST-LINK/V2 clone based on a STM32F101C8T6
- [USB-Blaster](https://wiki.cuvoodoo.info/doku.php?id=jtag#armjishu_usb-blaster), an Altera USB-Blaster clone based on a STM32F101C8T6
**Which board is used is defined in the Makefile**.
This is required to map the user LED and button provided on the board
The ST-LINK V2 mini clone has SWD test points on the board.
Because read protection is enabled, you will first need to remove the protection to be able to flash the firmware.
To remove the read protection (and erase flash), run `rake remove_protection` while a SWD adapter is connected.
The Altera USB-Blaster clone has a pin header for SWD and UART1 on the board.
SWD is disabled in the main firmware, and it has read protection.
To be able to flash using SWD (or the serial port), the BOOT0 pin must be set to 1 to boot the system memory install of the flash memory.
To set BOOT0 to 1, apply 3.3 V on R11, between the resistor and the reference designator, when powering the device.
The red LED should stay off while the green LED is on.
Now you can remove the read protection (and erase flash), run `rake remove_protection` while a SWD adapter is connected.
- a [blue pill](https://wiki.cuvoodoo.info/doku.php?id=stm32f1xx#blue_pill) development board, based on a STM32F103C8T6 micro-controller
- a GW1584 step down voltage converter module to lower the 12V input to 5V for all peripherals
- a switch to cut the power, allowing other devices to connect to the Bluetooth speakers
- a micro-switch in the door lock detects when a user closed the door to use the toilet in peace
- a SK6812 RGBW LED strip illuminates the shield to indicate if the toilet is free or occupied
- a Titan TM1637 4-digit 7-segment display shows the time is second passed since the door has been locked
- a Catalex YX5300 MP3 player reads the songs and announcements from a micro-SD card and outputs it to a 3.5 mm stereo jack
- a Taotronics TT-BA01 Bluetooth transmitter with input 3.5 mm stereo jack connects to the room speaker to send the audio
connections
===========
Connect the peripherals the following way (STM32F10X signal; STM32F10X pin; peripheral pin; peripheral signal; comment):
GW1584 step down voltage converter module:
- *list board to peripheral pin connections*
- IN-: outer shell of 5.5/2.1 barrel connector
- IN+: inner pin of 5.5/2.1 barrel connector, after power switch (6-12V)
- OUT-: ground
- OUT+: 5V
All pins are configured using `define`s in the corresponding source code.
micro-switch in door lock:
- COM: ground
- NO: PB8, use external pull-up resistor (10k to 5V)
SK6812 RGBW LED strip:
- 5V (green cable): 5V
- DIN (yellow cable): PB15 (SPI2_MOSI), through voltage shifter (to get clean 5V signal)
- GND (orange cable): GND
Titan TM1637 4-digit 7-segment display:
- GND: GND
- VCC: 5V
- DIO: PB7
- CLK: PB6
Catalex YX5300 MP3 player:
- GND: GND
- VCC: 5V
- TX: PA3 (UART2_RX)
- RX: PA2 (UART2_TX)
Taotronics TT-BA01:
- 3.5 mm stereo jack: to MP3 player
- cable soldered to button: PA0 (this allows to switch on/off the transmitter, and put it into pairing mode)
- cable soldered to blue: PA1 (this allow to know in which state the transmitter is)
code
====
@ -81,9 +106,9 @@ The `bootloader` is started first and immediately jumps to the `application` if
The `application` image is the main application and is implemented in `application.c`.
It is up to the application to advertise USB DFU support (i.e. as does the provided USB CDC ACM example).
The `bootlaoder` image will be flashed using SWD (Serial Wire Debug).
The `bootloader` image will be flashed using SWD (Serial Wire Debug).
For that you need an SWD adapter.
The `Makefile` uses a Black Magic Probe (per default), or a ST-Link V2 along OpenOCD software.
The `Makefile` uses a ST-Link V2 along OpenOCD software.
To flash the `booltoader` using SWD run `rake flash_booloader`.
Once the `bootloader` is flashed it is possible to flash the `application` over USB using the DFU protocol by running `rake flash`.

View File

@ -1,8 +1,8 @@
/** STM32F1 application example
/** dachboden klo-assistant firmware
* @file
* @author King Kévin <kingkevin@cuvoodoo.info>
* @copyright SPDX-License-Identifier: GPL-3.0-or-later
* @date 2016-2020
* @date 2016-2021
*/
/* standard libraries */
@ -24,6 +24,8 @@
#include <libopencm3/stm32/dbgmcu.h> // debug utilities
#include <libopencm3/stm32/desig.h> // design utilities
#include <libopencm3/stm32/flash.h> // flash utilities
#include <libopencm3/stm32/usart.h> // USART utilities for MP3 player
#include <libopencm3/stm32/adc.h> // ADC utilities for random seed
/* own libraries */
#include "global.h" // board definitions
@ -34,6 +36,8 @@
#include "usb_cdcacm.h" // USB CDC ACM utilities
#include "terminal.h" // handle the terminal interface
#include "menu.h" // menu utilities
#include "led_tm1637.h" // TM1637 7-segment display controller
#include "led_sk6812rgbw.h" // SK6812 RGBW LED controller
/** watchdog period in ms */
#define WATCHDOG_PERIOD 10000
@ -41,16 +45,12 @@
/** set to 0 if the RTC is reset when the board is powered on, only indicates the uptime
* set to 1 if VBAT can keep the RTC running when the board is unpowered, indicating the date and time
*/
#if defined(CORE_BOARD)
#define RTC_DATE_TIME 1
#else
#define RTC_DATE_TIME 0
#endif
/** number of RTC ticks per second
* @note use integer divider of oscillator to keep second precision
*/
#define RTC_TICKS_SECOND 4
#define RTC_TICKS_SECOND 16
#if defined(RTC_DATE_TIME) && RTC_DATE_TIME
/** the start time from which to RTC ticks count
@ -62,12 +62,122 @@ const time_t rtc_offset = 1577833200; // We 1. Jan 00:00:00 CET 2020
/** RTC time when device is started */
static time_t time_start = 0;
/** time when the door has been closed/locked (RTC time) */
static uint32_t timer_door_closed = 0;
/** @defgroup main_flags flag set in interrupts to be processed in main task
* @{
*/
static volatile bool rtc_internal_tick_flag = false; /**< flag set when internal RTC ticked */
static volatile bool mp3_rx_flag = false; /**< if data has been received from the MP3 player */
static volatile bool door_flag = false; /**< set when the door lock switch changes */
/** @} */
/** switch in the door to verify if its locked (closed when locked)
* @note use external pull-up resistor (10k to 5V) since the internal pull up does not cope for the long cable
*/
#define DOOR_PIN PB8
/** USART port used to communicate with catalex MP3 player */
#define MP3_UART 2
/** data received from MP3 player */
static volatile uint8_t mp3_rx_data[10];
/** number of byte received from MP3 player */
static volatile uint8_t mp3_rx_len;
/** pin to simulate button press (go high) to switch on/off Bluetooth audio transmitter
* - 2s press: on/off
* - 5s press when off: pair
*/
#define BTAUDIO_BUTTON_PIN PA0
/** status output (e.g. LED) of Bluetooth audio transmitter
* - off: off
* - fast flash (periodic blink every 0.3s): pairing
* - slow flash (double blink every 5.3s): unconnected
* - very slow flash (single blink every 10s): connected
*/
#define BTAUDIO_STATUS_PIN PA1
/** time (in ms) to press on the button to switch Bluetooth audio transmitter */
#define BTAUDIO_ON 3500
/** number of timer led Bluetooth audio transmitter blinked, to determine the status */
static uint8_t btaudio_status = 0;
/** catalex MP3 player commands */
enum mp3_commands_t {
MP3_CMD_NEXT_SONG = 0x01,
MP3_CMD_PREV_SONG = 0x02,
MP3_CMD_PLAY_W_INDEX = 0x03,
MP3_CMD_VOLUME_UP = 0x04,
MP3_CMD_VOLUME_DOWN = 0x05,
MP3_CMD_SET_VOLUME = 0x06,
MP3_CMD_SINGLE_CYCLE_PLAY = 0x08,
MP3_CMD_SEL_DEV = 0x09,
MP3_CMD_SLEEP_MODE = 0x0A,
MP3_CMD_WAKE_UP = 0x0B,
MP3_CMD_RESET = 0x0C,
MP3_CMD_PLAY = 0x0D,
MP3_CMD_PAUSE = 0x0E,
MP3_CMD_PLAY_FOLDER_FILE = 0x0F,
MP3_CMD_STOP_PLAY = 0x16,
MP3_CMD_FOLDER_CYCLE = 0x17,
MP3_CMD_SHUFFLE_PLAY = 0x18,
MP3_CMD_SET_SINGLE_CYCLE = 0x19,
MP3_CMD_SET_DAC = 0x1A,
MP3_CMD_PLAY_W_VOL = 0x22,
MP3_CMD_QUERY_STATUS = 0x42,
MP3_CMD_QUERY_FLDR_TRACKS = 0x4e,
MP3_CMD_QUERY_TOT_TRACKS = 0x48,
MP3_CMD_QUERY_FLDR_COUNT = 0x4f,
};
/** which song group we are currently playing */
static enum playing_state_t {
PLAYING_STATE_OFF, /**< playing any song or track */
PLAYING_STATE_INTRO, /**< playing the welcome message */
PLAYING_STATE_SONG, /**< playing any song */
PLAYING_STATE_TIMER_INTRO, /**< playing the time announcement intro */
PLAYING_STATE_TIMER_MINUTES, /**< playing the number of minutes */
PLAYING_STATE_TIMER_MINUTE, /**< playing the minute announcement */
PLAYING_STATE_TIMER_SECONDS, /**< playing the number of seconds */
PLAYING_STATE_TIMER_OUTRO, /**< playing the timer announcement closing word */
PLAYING_STATE_TALK, /**< playing any talk track */
PLAYING_STATE_EXIT, /**< playing any exit message */
PLAYING_STATE_TECHNO, /**< continuously play techno songs (use secret open/close sequence to enter modus) */
PLAYING_STATE_KANGURU, /**< continuously play kanguru sketches (use secret open/close/open/close sequence to enter modus) */
} playing_state = PLAYING_STATE_OFF; /**< which song group we are currently playing */
/** RTC timestamps when the last MP3 response track finished has been received */
static uint32_t last_finished = 0;
/** number of possible welcome tacks */
#define WELCOME_TRACKS 15
/** number of possible music tracks */
#define MUSIC_TRACKS 30
/** number of possible exit message tracks */
#define EXIT_TRACKS 19
/** number of possible talk tracks */
#define TALK_TRACKS 29
/** number of possible techno music tracks */
#define TECHNO_TRACKS 237
/** number of possible kanguru */
#define KANGURU_TRACKS 81
/** folder number for welcome tracks */
#define WELCOME_FOLDER 1
/** folder number for music tracks */
#define MUSIC_FOLDER 2
/** folder number for exit messages tracks */
#define EXIT_FOLDER 4
/** folder number for talk tracks */
#define TALK_FOLDER 3
/** folder number for time number announcement tracks */
#define TIME_FOLDER 5
/** folder number for techno music tracks */
#define TECHNO_FOLDER 6
/** folder number for kanguru tracks */
#define KANGURU_FOLDER 7
size_t putc(char c)
{
size_t length = 0; // number of characters printed
@ -122,6 +232,271 @@ static void command_reset(void* argument);
*/
static void command_bootloader(void* argument);
/** get random track from folder
* @param[in] folder MP3 SD card folder number
* @return a track within the folder (0 on invalid folder)
*/
static uint16_t track_random(uint8_t folder)
{
uint8_t tracks; // number of available tracks in folder
switch (folder) {
case WELCOME_FOLDER:
tracks = WELCOME_TRACKS;
break;
case MUSIC_FOLDER:
tracks = MUSIC_TRACKS;
break;
case EXIT_FOLDER:
tracks = EXIT_TRACKS;
break;
case TALK_FOLDER:
tracks = TALK_TRACKS;
break;
case TECHNO_FOLDER:
tracks = TECHNO_TRACKS;
break;
case KANGURU_FOLDER:
tracks = KANGURU_TRACKS;
break;
default: // invalid folder
return 0;
}
static uint16_t played[10] = {0}; // the last tracks played
bool track_ok = false; // when we found a valid track number
uint16_t track_nr; // the track we will play
while (!track_ok) {
track_nr = (folder << 8) + (rand() % tracks) + 1; // generate random track number
track_ok = true;
for (uint8_t i = 0; i < LENGTH(played); i++) { // go though played list
if (track_nr == played[i]) { // we already played the track
track_ok = false; // the track we have is not OK
break;
}
}
}
for (uint8_t i = 0; i < LENGTH(played) - 1; i++) { // shift playlist
played[i + 1] = played[i];
}
played[0] = track_nr; // save the track we chose
return track_nr; // return the random track within folder
}
/** send command to MP3 playes
* @param[in] cmd command to send
* @param[in] data argument for command (such as track number)
*/
static void mp3_command(enum mp3_commands_t cmd, uint16_t data)
{
puts("MP3 command: ");
switch (cmd) {
case MP3_CMD_PLAY:
puts("play");
break;
case MP3_CMD_NEXT_SONG:
puts("next");
break;
case MP3_CMD_STOP_PLAY:
puts("stop");
break;
case MP3_CMD_PLAY_FOLDER_FILE:
puts("playing ");
const uint8_t folder = data >> 8;
const uint8_t track = data & 0xff;
printf("%02u/%03u", folder, track);
if (0 == folder) {
puts(" (invalid input folder 0)");
}
if (0 == track) {
puts(" (invalid input track 0");
}
break;
case MP3_CMD_SET_VOLUME:
printf("set volume to %u", data);
break;
case MP3_CMD_RESET:
puts("reset");
break;
default:
printf("%+02x");
break;
}
putc('\n');
uint8_t command[] = {0x7e, 0xff, 0x06, cmd, 0x01, data >> 8, data, 0xef}; // command template (with feedback)
for (uint8_t i = 0; i < LENGTH(command); i++) {
usart_send_blocking(USART(MP3_UART), command[i]);
}
}
/** process response received from MP3 player
* @return if a valid response has been received
*/
static bool mp3_response(void)
{
if (mp3_rx_len < 10) { // responses are always at least 10 bytes long
return false;
}
if (0x7e != mp3_rx_data[0]) { // response always starts with 0x7e
mp3_rx_len = 0; // reset message
return false;
}
if (0xff != mp3_rx_data[1]) { // version is always 0xff
mp3_rx_len = 0; // reset message
return false;
}
if (mp3_rx_data[2] > LENGTH(mp3_rx_data) - 3) { // message is longer than buffer
mp3_rx_len = 0; // reset message
return false;
}
if (mp3_rx_data[2] < mp3_rx_len - 2U - 2U) { // message is not complete
return false;
}
if (0xef != mp3_rx_data[3 + mp3_rx_data[2]]) { // end by is not correct
return false;
}
puts("MP3 response: ");
/*
// display message
puts("> ");
for (uint8_t i = 0; i < mp3_rx_len; i++) {
printf("%02x ", mp3_rx_data[i]);
}
putc('\n');
*/
// check checksum
int16_t checksum = 0;
for (uint8_t i = 1; i < mp3_rx_len && i < mp3_rx_data[2] + 1U; i++) {
checksum += mp3_rx_data[i];
}
checksum = -checksum;
const int16_t expected = ((mp3_rx_data[mp3_rx_data[2] + 1] << 8) + mp3_rx_data[mp3_rx_data[2] + 2]);
if (expected != checksum) {
printf("wrong checksum: should=%+04x is=%+04x\n", expected, checksum);
mp3_rx_len = 0; // reset message
return false;
}
switch (mp3_rx_data[3]) {
case 0x3a:
puts("card inserted");
break;
case 0x3b:
puts("card removed");
break;
case 0x3d:
puts("track finished");
if ((rtc_get_counter_val() - last_finished) < 2) {
const uint16_t time_passed = (rtc_get_counter_val() - timer_door_closed) / RTC_TICKS_SECOND; // how many seconds have passed since door has been closed
switch (playing_state) {
case PLAYING_STATE_INTRO: // the welcome message finished
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, track_random(MUSIC_FOLDER)); // play random music track
playing_state = PLAYING_STATE_SONG; // remember we are playing a song (for the first time)
break;
case PLAYING_STATE_SONG: // the song finished
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, (TIME_FOLDER << 8) + 65); // play time announcement
playing_state = PLAYING_STATE_TIMER_INTRO; // remember we are playing the timer announcement
break;
case PLAYING_STATE_TIMER_INTRO: // the time intro finished
if (0 == (time_passed / 60)) {
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, (TIME_FOLDER << 8) + 60); // play number of minutes announcement
} else {
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, (TIME_FOLDER << 8) + (time_passed / 60)); // play number of minutes announcement
}
playing_state = PLAYING_STATE_TIMER_MINUTES; // remember we are playing the number of minutes
break;
case PLAYING_STATE_TIMER_MINUTES: // the minutes announcement finished
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, (TIME_FOLDER << 8) + 62); // play minute announcement
playing_state = PLAYING_STATE_TIMER_MINUTE; // remember we are playing the minute announcement
break;
case PLAYING_STATE_TIMER_MINUTE: // the minute announcement finished
if (0 == (time_passed % 60)) {
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, (TIME_FOLDER << 8) + 60); // play number of seconds announcement
} else {
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, (TIME_FOLDER << 8) + (time_passed % 60)); // play number of seconds announcement
}
playing_state = PLAYING_STATE_TIMER_SECONDS; // remember we are playing the number of seconds
break;
case PLAYING_STATE_TIMER_SECONDS: // the number of seconds finished
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, (TIME_FOLDER << 8) + 64); // play time outro announcement
playing_state = PLAYING_STATE_TIMER_OUTRO; // remember we are playing the timer outro announcement
break;
case PLAYING_STATE_TIMER_OUTRO: // the timer outro announcement finished
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, track_random(TALK_FOLDER)); // play random talk track
playing_state = PLAYING_STATE_TALK; // remember we are playing the talk track
break;
case PLAYING_STATE_TALK: // the talk track finished
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, track_random(MUSIC_FOLDER)); // play random music track
playing_state = PLAYING_STATE_SONG; // remember we are playing a song (again)
break;
case PLAYING_STATE_TECHNO: // techno song completed, play the next one
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, track_random(TECHNO_FOLDER)); // play random techno music track
break;
case PLAYING_STATE_KANGURU: // kanguru sketch completed, play the next one
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, track_random(KANGURU_FOLDER)); // play random techno music track
break;
default:
playing_state = PLAYING_STATE_OFF; // we won't play anything else
break;
}
}
last_finished = rtc_get_counter_val(); // remember last time we received the ack
break;
case 0x40:
puts("error");
led_tm1637_text("sder"); // show we have an issue with the SD card
led_tm1637_on(); // switch on display
break;
case 0x41:
puts("ack");
break;
case 0x4e:
puts("track count");
break;
default:
printf("unknown: %+02x", mp3_rx_data[3]);
break;
}
putc('\n');
mp3_rx_len = 0; // reset message
return true;
}
/** play MP3
* @param[in] argument no argument required
*/
static void command_play(void* argument)
{
if (NULL == argument) {
puts("playing first track\n");
mp3_command(MP3_CMD_PLAY, 1); // play first song
} else {
const uint32_t track = *(uint32_t*)argument; // argument not used
printf("playing track %u\n", track);
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, ((track / 100) << 8) + (track % 100)); // play specific track
}
}
/** put Bluetooth audi transmitter into pairing mode
* @param[in] argument no argument required
*/
static void command_pair(void* argument)
{
(void)argument; // argument not used
// we assume the transmitter is on
puts("switching BT audio off\n");
gpio_set(GPIO_PORT(BTAUDIO_BUTTON_PIN), GPIO_PIN(BTAUDIO_BUTTON_PIN)); // press button to switch off
sleep_ms(BTAUDIO_ON); // keep pressed
gpio_clear(GPIO_PORT(BTAUDIO_BUTTON_PIN), GPIO_PIN(BTAUDIO_BUTTON_PIN)); // release button
puts("putting BT audio into pairing mode\n");
sleep_ms(1000); // wait a bit
gpio_set(GPIO_PORT(BTAUDIO_BUTTON_PIN), GPIO_PIN(BTAUDIO_BUTTON_PIN)); // press button to put into pairing mode
sleep_ms(6000); // keep pressed
gpio_clear(GPIO_PORT(BTAUDIO_BUTTON_PIN), GPIO_PIN(BTAUDIO_BUTTON_PIN)); // release button
puts("BT transmitter should be looking for speaker\n");
}
/** list of all supported commands */
static const struct menu_command_t menu_commands[] = {
{
@ -174,6 +549,22 @@ static const struct menu_command_t menu_commands[] = {
.argument_description = NULL,
.command_handler = &command_bootloader,
},
{
.shortcut = 'p',
.name = "play",
.command_description = "play MP3 song",
.argument = MENU_ARGUMENT_UNSIGNED,
.argument_description = "track folder+number fftt",
.command_handler = &command_play,
},
{
.shortcut = 'a',
.name = "audio",
.command_description = "put BT audio transmitter into pairing mode",
.argument = MENU_ARGUMENT_NONE,
.argument_description = NULL,
.command_handler = &command_pair,
},
};
static void command_help(void* argument)
@ -273,6 +664,19 @@ static void process_command(char* str)
}
}
/** switch LED sign to besetzt/occupied
* @param[in] besetzt if the sign should light up besetzt/occupied (true) or frei/free(false)
*/
static void leds_sign(bool besetzt)
{
for (uint8_t led = 0; led < LED_SK6812RGBW_LEDS / 2; led++) {
led_sk6812rgbw_set_rgb(led, 0, besetzt ? 0 : 0xff, 0, 0);
}
for (uint8_t led = LED_SK6812RGBW_LEDS / 2; led < LED_SK6812RGBW_LEDS; led++) {
led_sk6812rgbw_set_rgb(led, besetzt ? 0xff : 0 , 0, 0, 0);
}
}
/** program entry point
* this is the firmware function started by the micro-controller
*/
@ -299,7 +703,7 @@ void main(void)
uart_setup(); // setup USART (for printing)
#endif
usb_cdcacm_setup(); // setup USB CDC ACM (for printing)
puts("\nwelcome to the CuVoodoo STM32F1 example application\n"); // print welcome message
puts("\nwelcome to the CuVoodoo Dachboden Klo-Assistant\n"); // print welcome message
#if DEBUG
// show reset cause
@ -352,6 +756,112 @@ void main(void)
time_start = rtc_get_counter_val(); // get start time from internal RTC
puts("OK\n");
puts("setup TM1637 7-segment display: ");
bool led_tm1637_setup_ok = true;
led_tm1637_setup(true);
led_tm1637_setup_ok &= led_tm1637_brightness(LED_TM1637_14DIV16); // set maximum brightness
led_tm1637_setup_ok &= led_tm1637_time(88, 88); // light up all segments
led_tm1637_setup_ok &= led_tm1637_on(); // switch on to test it
if (led_tm1637_setup_ok) {
puts("OK\n");
} else {
puts("error\n");
}
puts("setup Bluetooth audio: ");
rcc_periph_clock_enable(GPIO_RCC(BTAUDIO_BUTTON_PIN)); // enable clock for button
gpio_clear(GPIO_PORT(BTAUDIO_BUTTON_PIN), GPIO_PIN(BTAUDIO_BUTTON_PIN)); // set as unpressed
gpio_set_mode(GPIO_PORT(BTAUDIO_BUTTON_PIN), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_PIN(BTAUDIO_BUTTON_PIN)); // set button pin as output
gpio_set(GPIO_PORT(BTAUDIO_BUTTON_PIN), GPIO_PIN(BTAUDIO_BUTTON_PIN)); // press button to switch on
sleep_ms(BTAUDIO_ON); // keep pressed
gpio_clear(GPIO_PORT(BTAUDIO_BUTTON_PIN), GPIO_PIN(BTAUDIO_BUTTON_PIN)); // release button
rcc_periph_clock_enable(GPIO_RCC(BTAUDIO_STATUS_PIN)); // enable clock for GPIO peripheral to read status input
gpio_set_mode(GPIO_PORT(BTAUDIO_STATUS_PIN), GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO_PIN(BTAUDIO_STATUS_PIN)); // set status pin as input
exti_set_trigger(GPIO_EXTI(BTAUDIO_STATUS_PIN), EXTI_TRIGGER_FALLING); // trigger when LED goes on
exti_enable_request(GPIO_EXTI(BTAUDIO_STATUS_PIN)); // enable external interrupt
nvic_enable_irq(GPIO_NVIC_EXTI_IRQ(BTAUDIO_STATUS_PIN)); // enable interrupt
btaudio_status = 0; // number of LED blinks received, to determine the BT audio state
puts("OK\n");
led_tm1637_setup_ok &= led_tm1637_off(); // switch off and let main loop handle it
// setup LEDs
puts("setup SK6812RGBW LEDs: ");
led_sk6812rgbw_setup();
for (uint8_t led = 0; led < LED_SK6812RGBW_LEDS; led++) {
led_sk6812rgbw_set_rgb(led, 0, 0, 0, 64); // switch white on
}
sleep_ms(1000); // give user time to verify
for (uint8_t led = 0; led < LED_SK6812RGBW_LEDS; led++) {
led_sk6812rgbw_set_rgb(led, 0, 0, 0, 0); // switch all off
}
leds_sign(0 != timer_door_closed);
puts("OK\n");
puts("setup catalex YX5300 MP3 player: ");
rcc_periph_clock_enable(RCC_USART_PORT(MP3_UART)); // enable clock for UART port peripheral
rcc_periph_clock_enable(RCC_USART(MP3_UART)); // enable clock for UART peripheral
rcc_periph_reset_pulse(RST_USART(MP3_UART)); // reset peripheral
rcc_periph_clock_enable(RCC_AFIO); // enable pin alternate function (UART)
gpio_set_mode(USART_TX_PORT(MP3_UART), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, USART_TX_PIN(MP3_UART)); // setup GPIO pin UART transmit
gpio_set_mode(USART_RX_PORT(MP3_UART), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, USART_RX_PIN(MP3_UART)); // setup GPIO pin UART receive
gpio_set(USART_RX_PORT(MP3_UART), USART_RX_PIN(MP3_UART)); // pull up to avoid noise when not connected
usart_set_baudrate(USART(MP3_UART), 9600);
usart_set_databits(USART(MP3_UART), 8);
usart_set_stopbits(USART(MP3_UART), USART_STOPBITS_1);
usart_set_mode(USART(MP3_UART), USART_MODE_TX_RX);
usart_set_parity(USART(MP3_UART), USART_PARITY_NONE);
usart_set_flow_control(USART(MP3_UART), USART_FLOWCONTROL_NONE);
usart_enable_rx_interrupt(USART(MP3_UART)); // enable receive interrupt
nvic_enable_irq(USART_IRQ(MP3_UART)); // enable the UART interrupt
usart_enable(USART(MP3_UART)); // enable UART
puts("OK\n");
mp3_command(MP3_CMD_RESET, 0); // reset all settings
sleep_ms(500);
mp3_command(MP3_CMD_SEL_DEV, 2); // set volume lower (BT audio transmitter saturates)
sleep_ms(500);
mp3_command(MP3_CMD_SET_VOLUME, 15); // set volume lower (BT audio transmitter saturates)
puts("setup door lock switch: ");
rcc_periph_clock_enable(GPIO_RCC(DOOR_PIN)); // enable clock for button
gpio_set_mode(GPIO_PORT(DOOR_PIN), GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO_PIN(DOOR_PIN)); // set button pin to input
rcc_periph_clock_enable(RCC_AFIO); // enable alternate function clock for external interrupt
exti_select_source(GPIO_EXTI(DOOR_PIN), GPIO_PORT(DOOR_PIN)); // mask external interrupt of this pin only for this port
gpio_set(GPIO_PORT(DOOR_PIN), GPIO_PIN(DOOR_PIN)); // pull up to be able to detect button push (go low)
exti_set_trigger(GPIO_EXTI(DOOR_PIN), EXTI_TRIGGER_BOTH); // trigger on change
exti_enable_request(GPIO_EXTI(DOOR_PIN)); // enable external interrupt
nvic_enable_irq(GPIO_NVIC_EXTI_IRQ(DOOR_PIN)); // enable interrupt
uint32_t door_sequence[4] = {0}; // duration of past door states, in RTC ticks, oldest first, updated every time the door is opened/closed
uint32_t door_timestamp = 0; // RTC timestamps when the door state has changed
puts("OK\n");
puts("setup RNG: ");
rcc_periph_clock_enable(RCC_ADC1); // enable clock for ADC peripheral
adc_power_off(ADC1); // ensure ADC is off for configuring
rcc_periph_reset_pulse(RST_ADC1); // reset configuration
rcc_set_adcpre(RCC_CFGR_ADCPRE_PCLK2_DIV2); // set clock
adc_set_dual_mode(ADC_CR1_DUALMOD_IND); // not sure what it does
adc_disable_scan_mode(ADC1); // we only do single conversion
adc_set_single_conversion_mode(ADC1); // ensure we do single conversion
adc_set_sample_time(ADC1, ADC_CHANNEL_TEMP, ADC_SMPR_SMP_1DOT5CYC); // we will read the temperature
adc_enable_external_trigger_regular(ADC1, ADC_CR2_EXTSEL_SWSTART); // conversion is triggered by software
adc_power_on(ADC1); // start ADC
adc_reset_calibration(ADC1); // reset calibration value
adc_calibrate(ADC1); // calibrate ADC
unsigned int seed = 1; // the seed we need to generate
uint8_t seed_rounds = 25; // get plenty of sample
while (seed_rounds--) { // go through all rounds
adc_start_conversion_regular(ADC1); // do conversion
while (!adc_eoc(ADC1)); // wait for conversion
const uint16_t temp = adc_read_regular(ADC1); // read temperature
if (temp > 2) {
seed *= temp; // calculate the seed
}
sleep_ms(10); // wait a bit for the temperature to change
}
srand(seed); // set the seed
printf("%u\n", seed);
// setup terminal
terminal_prefix = ""; // set default prefix
terminal_process = &process_command; // set central function to process commands
@ -359,7 +869,6 @@ void main(void)
// start main loop
bool action = false; // if an action has been performed don't go to sleep
button_flag = false; // reset button flag
while (true) { // infinite loop
iwdg_reset(); // kick the dog
if (user_input_available) { // user input is available
@ -368,19 +877,95 @@ void main(void)
char c = user_input_get(); // store receive character
terminal_send(c); // send received character to terminal
}
if (button_flag) { // user pressed button
action = true; // action has been performed
puts("button pressed\n");
led_toggle(); // toggle LED
sleep_ms(100); // wait a bit to remove noise and double trigger
button_flag = false; // reset flag
}
if (rtc_internal_tick_flag) { // the internal RTC ticked
rtc_internal_tick_flag = false; // reset flag
action = true; // action has been performed
if (0 == (rtc_get_counter_val() % RTC_TICKS_SECOND)) { // one seond has passed
if (0 == (rtc_get_counter_val() % RTC_TICKS_SECOND)) { // one second has passed
led_toggle(); // toggle LED (good to indicate if main function is stuck)
}
if (0 == (rtc_get_counter_val() % (RTC_TICKS_SECOND * 11))) { // 11 seconds have passed, time to check BT audio state
static bool bt_search = false; // set when searching audio device
if (0 == btaudio_status) {
puts("BT audio off, switching on\n");
gpio_set(GPIO_PORT(BTAUDIO_BUTTON_PIN), GPIO_PIN(BTAUDIO_BUTTON_PIN)); // press button to switch on
sleep_ms(BTAUDIO_ON); // keep pressed
gpio_clear(GPIO_PORT(BTAUDIO_BUTTON_PIN), GPIO_PIN(BTAUDIO_BUTTON_PIN)); // release button
led_tm1637_text("bton"); // show on display we are switching Bluetooth on
led_tm1637_on(); // switch on display
} else if (btaudio_status <= 2) {
puts("BT audio connected\n");
if (bt_search) { // we just searched before
led_tm1637_off(); // stop displaying we are searching
bt_search = false; // clear flag
}
} else if (btaudio_status <= 6) {
puts("BT audio disconnected\n");
led_tm1637_text("btof"); // show on display we are disconnected
led_tm1637_on(); // switch on display
bt_search = true; // remember we are searching (no actively re-pairing)
} else {
puts("BT audio searching\n");
led_tm1637_text("btsc"); // show on display we are searching
led_tm1637_on(); // switch on display
bt_search = true; // remember we are searching (with re-pairing)
}
btaudio_status = 0; // reset counter
}
if (timer_door_closed && 0 == ((rtc_get_counter_val() - timer_door_closed) % (RTC_TICKS_SECOND / 4))) { // 1/4 second has passed since since door closed
const uint16_t time_passed = (rtc_get_counter_val() - timer_door_closed) / RTC_TICKS_SECOND; // how many seconds have passed since door has been closed
led_tm1637_time(time_passed / 60, time_passed % 60); // show time passed
}
}
if (door_flag) { // door switch state changed
sleep_ms(100); // wait a bit to de-noise before we check the door lock state
const bool closed = (0 == gpio_get(GPIO_PORT(DOOR_PIN), GPIO_PIN(DOOR_PIN))); // get door lock state
door_flag = false; // clear flag
const uint32_t change_timestamp = rtc_get_counter_val(); // save when the door has been open/closed
action = true; // action has been performed
if (closed && 0 == timer_door_closed) { // door has been closed
puts("door closed\n");
timer_door_closed = rtc_get_counter_val(); // remember when the door has been closed
leds_sign(true); // show on the sign that the toilet is occupied
led_tm1637_time(0, 0); // start showing time on display
led_tm1637_on(); // ensure the display is on
// update door change sequence
for (uint8_t i = 0; i < LENGTH(door_sequence) - 1; i++) {
door_sequence[i] = door_sequence[i + 1]; // shift previous changes
}
door_sequence[LENGTH(door_sequence) - 1] = change_timestamp - door_timestamp; // not underflow safe
door_timestamp = change_timestamp; // remember the change
// depending on the open/close sequence, enter secret mode
if (door_sequence[LENGTH(door_sequence) - 1] < (2 * RTC_TICKS_SECOND) && door_sequence[LENGTH(door_sequence) - 2] < (2 * RTC_TICKS_SECOND) && door_sequence[LENGTH(door_sequence) - 3] < (2 * RTC_TICKS_SECOND) && door_sequence[LENGTH(door_sequence) - 4] < (2 * RTC_TICKS_SECOND)) { // sequence detected CLOSED->OPENED->CLOSED->OPEN->CLOSED, each within 2 seconds, then enter secret mode 2
puts("entering secret kanguru mode\n");
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, (KANGURU_FOLDER << 8) + (rtc_get_counter_val() % KANGURU_TRACKS) + 1); // play random kanguru sketch track
playing_state = PLAYING_STATE_KANGURU; // remember we are playing kanguru sketches
} else if (door_sequence[LENGTH(door_sequence) - 1] < (2 * RTC_TICKS_SECOND) && door_sequence[LENGTH(door_sequence) - 2] < (2 * RTC_TICKS_SECOND)) { // sequence detected CLOSED->OPENED->CLOSED, each within 2 seconds, then enter secret mode 1
puts("entering secret techno mode\n");
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, (TECHNO_FOLDER << 8) + (rtc_get_counter_val() % TECHNO_TRACKS) + 1); // play random techno music track
playing_state = PLAYING_STATE_TECHNO; // remember we are playing techno music
} else { // no secret mode sequence detected
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, (WELCOME_FOLDER << 8) + (rtc_get_counter_val() % WELCOME_TRACKS) + 1); // play random welcome track
playing_state = PLAYING_STATE_INTRO; // remember we are playing the welcome message
}
} else if (!closed && timer_door_closed) { // door has been opened
puts("door opened\n");
timer_door_closed = 0; // remember door is now open
leds_sign(false); // show on sign the toilet is free
led_tm1637_off(); // stop showing time
mp3_command(MP3_CMD_PLAY_FOLDER_FILE, (EXIT_FOLDER << 8) + (rtc_get_counter_val() % EXIT_TRACKS) + 1); // play random exit message track
playing_state = PLAYING_STATE_EXIT; // we are playing the exit track
// update door change sequence
for (uint8_t i = 0; i < LENGTH(door_sequence) - 1; i++) {
door_sequence[i] = door_sequence[i + 1]; // shift previous changes
}
door_sequence[LENGTH(door_sequence) - 1] = change_timestamp - door_timestamp; // not underflow safe
door_timestamp = change_timestamp; // remember the change
}
}
if (mp3_rx_flag) { // data from MP3 player received
mp3_response(); // check for response
mp3_rx_flag = false; // clear flag
action = true; // action has been performed
}
if (action) { // go to sleep if nothing had to be done, else recheck for activity
action = false;
@ -396,3 +981,30 @@ void rtc_isr(void)
rtc_clear_flag(RTC_SEC); // clear flag
rtc_internal_tick_flag = true; // notify to show new time
}
/** UART interrupt service routine called when data has been received */
void USART_ISR(MP3_UART)(void)
{
if (usart_get_flag(USART(MP3_UART), USART_SR_RXNE)) { // data has been transmitted
if (mp3_rx_len < LENGTH(mp3_rx_data)) {
mp3_rx_data[mp3_rx_len++] = usart_recv(USART(MP3_UART)); // store received data
} else {
usart_recv(USART(MP3_UART)); // discard data
}
mp3_rx_flag = true; // notify user
}
}
/** door has been opened/closed */
void GPIO_EXTI_ISR(DOOR_PIN)(void)
{
exti_reset_request(GPIO_EXTI(DOOR_PIN)); // reset interrupt/clear flag
door_flag = true; // notify main loop
}
/** Bluetooth audio transmitter switched on LED */
void GPIO_EXTI_ISR(BTAUDIO_STATUS_PIN)(void)
{
exti_reset_request(GPIO_EXTI(BTAUDIO_STATUS_PIN)); // reset interrupt/clear flag
btaudio_status++; // count for main loop
}

View File

@ -1,598 +0,0 @@
/** library to communicate with an SD card flash memory using the SPI mode (code)
* @file
* @author King Kévin <kingkevin@cuvoodoo.info>
* @copyright SPDX-License-Identifier: GPL-3.0-or-later
* @date 2017
* @note peripherals used: SPI @ref flash_sdcard_spi
* @warning all calls are blocking
* @implements SD Specifications, Part 1, Physical Layer, Simplified Specification, Version 6.00, 10 April 10 2017
* @todo use SPI unidirectional mode, use DMA, force/wait going to idle state when initializing, filter out reserved values, check sector against size
*/
/* standard libraries */
#include <stdint.h> // standard integer types
#include <stdlib.h> // general utilities
/* STM32 (including CM3) libraries */
#include <libopencmsis/core_cm3.h> // Cortex M3 utilities
#include <libopencm3/stm32/rcc.h> // real-time control clock library
#include <libopencm3/stm32/gpio.h> // general purpose input output library
#include <libopencm3/stm32/spi.h> // SPI library
#include "global.h" // global utilities
#include "flash_sdcard.h" // SD card header and definitions
/** @defgroup flash_sdcard_spi SPI used to communication with SD card
* @{
*/
#define FLASH_SDCARD_SPI 1 /**< SPI peripheral */
/** @} */
/** if the card has been initialized successfully */
static bool initialized = false;
/** maximum N_AC value (in 8-clock cycles) (time between the response token R1 and data block when reading data (see section 7.5.4)
* @note this is set to N_CR until we can read CSD (see section 7.2.6)
*/
static uint32_t n_ac = 8;
/** is it a Standard Capacity SD card (true), or High Capacity SD cards (false)
* @note this is indicated in the Card Capacity Status bit or OCR (set for high capacity)
* @note this is important for addressing: for standard capacity cards the address is the byte number, for high capacity cards it is the 512-byte block number
*/
static bool sdsc = false;
/** size of card in bytes */
static uint64_t sdcard_size = 0;
/** size of an erase block bytes */
static uint32_t erase_size = 0;
/** table for CRC-7 calculation for the command messages (see section 4.5)
* @note faster than calculating the CRC and doesn't cost a lot of space
* @note generated using pycrc --width=7 --poly=0x09 --reflect-in=false --reflect-out=false --xor-in=0x00 --xor-out=0x00 --generate=table
*/
static const uint8_t crc7_table[] = {
0x00, 0x09, 0x12, 0x1b, 0x24, 0x2d, 0x36, 0x3f, 0x48, 0x41, 0x5a, 0x53, 0x6c, 0x65, 0x7e, 0x77,
0x19, 0x10, 0x0b, 0x02, 0x3d, 0x34, 0x2f, 0x26, 0x51, 0x58, 0x43, 0x4a, 0x75, 0x7c, 0x67, 0x6e,
0x32, 0x3b, 0x20, 0x29, 0x16, 0x1f, 0x04, 0x0d, 0x7a, 0x73, 0x68, 0x61, 0x5e, 0x57, 0x4c, 0x45,
0x2b, 0x22, 0x39, 0x30, 0x0f, 0x06, 0x1d, 0x14, 0x63, 0x6a, 0x71, 0x78, 0x47, 0x4e, 0x55, 0x5c,
0x64, 0x6d, 0x76, 0x7f, 0x40, 0x49, 0x52, 0x5b, 0x2c, 0x25, 0x3e, 0x37, 0x08, 0x01, 0x1a, 0x13,
0x7d, 0x74, 0x6f, 0x66, 0x59, 0x50, 0x4b, 0x42, 0x35, 0x3c, 0x27, 0x2e, 0x11, 0x18, 0x03, 0x0a,
0x56, 0x5f, 0x44, 0x4d, 0x72, 0x7b, 0x60, 0x69, 0x1e, 0x17, 0x0c, 0x05, 0x3a, 0x33, 0x28, 0x21,
0x4f, 0x46, 0x5d, 0x54, 0x6b, 0x62, 0x79, 0x70, 0x07, 0x0e, 0x15, 0x1c, 0x23, 0x2a, 0x31, 0x38,
0x41, 0x48, 0x53, 0x5a, 0x65, 0x6c, 0x77, 0x7e, 0x09, 0x00, 0x1b, 0x12, 0x2d, 0x24, 0x3f, 0x36,
0x58, 0x51, 0x4a, 0x43, 0x7c, 0x75, 0x6e, 0x67, 0x10, 0x19, 0x02, 0x0b, 0x34, 0x3d, 0x26, 0x2f,
0x73, 0x7a, 0x61, 0x68, 0x57, 0x5e, 0x45, 0x4c, 0x3b, 0x32, 0x29, 0x20, 0x1f, 0x16, 0x0d, 0x04,
0x6a, 0x63, 0x78, 0x71, 0x4e, 0x47, 0x5c, 0x55, 0x22, 0x2b, 0x30, 0x39, 0x06, 0x0f, 0x14, 0x1d,
0x25, 0x2c, 0x37, 0x3e, 0x01, 0x08, 0x13, 0x1a, 0x6d, 0x64, 0x7f, 0x76, 0x49, 0x40, 0x5b, 0x52,
0x3c, 0x35, 0x2e, 0x27, 0x18, 0x11, 0x0a, 0x03, 0x74, 0x7d, 0x66, 0x6f, 0x50, 0x59, 0x42, 0x4b,
0x17, 0x1e, 0x05, 0x0c, 0x33, 0x3a, 0x21, 0x28, 0x5f, 0x56, 0x4d, 0x44, 0x7b, 0x72, 0x69, 0x60,
0x0e, 0x07, 0x1c, 0x15, 0x2a, 0x23, 0x38, 0x31, 0x46, 0x4f, 0x54, 0x5d, 0x62, 0x6b, 0x70, 0x79
};
/** wait one SPI round (one SPI word)
*/
static void flash_sdcard_spi_wait(void)
{
spi_send(SPI(FLASH_SDCARD_SPI), 0xffff); // send not command token (i.e. starting with 1)
}
/** read one SPI word
* @return SPI word read
*/
static uint16_t flash_sdcard_spi_read(void)
{
spi_send(SPI(FLASH_SDCARD_SPI), 0xffff); // send not command token (i.e. starting with 1)
(void)SPI_DR(SPI(FLASH_SDCARD_SPI)); // clear RXNE flag (by reading previously received data (not done by spi_read or spi_xref)
while (!(SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_TXE)); // wait until Tx buffer is empty before clearing the (previous) RXNE flag
while (!(SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_RXNE)); // wait for next data to be available
return SPI_DR(SPI(FLASH_SDCARD_SPI)); // return received adat
}
/** test if card is present
* @return if card has been detected
* @note this use the SD card detection mechanism (CD/CS is high card is inserted due to the internal 50 kOhm resistor)
*/
static bool flash_sdcard_card_detect(void)
{
rcc_periph_clock_enable(RCC_SPI_NSS_PORT(FLASH_SDCARD_SPI)); // enable clock for NSS pin port peripheral for SD card CD signal
gpio_set_mode(SPI_NSS_PORT(FLASH_SDCARD_SPI), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, SPI_NSS_PIN(FLASH_SDCARD_SPI)); // set NSS pin as input to read CD signal
gpio_clear(SPI_NSS_PORT(FLASH_SDCARD_SPI), SPI_NSS_PIN(FLASH_SDCARD_SPI)); // pull pin low to avoid false positive when card in not inserted
return (0!=gpio_get(SPI_NSS_PORT(FLASH_SDCARD_SPI), SPI_NSS_PIN(FLASH_SDCARD_SPI))); // read CD signal: is card is present the internal 50 kOhm pull-up resistor will override our 1 MOhm pull-down resistor and set the signal high (see section 6.2)
}
/** transmit command token
* @param[in] index command index
* @param[in] argument command argument
*/
static void flash_sdcard_send_command(uint8_t index, uint32_t argument)
{
uint8_t command[5] = { 0x40+(index&0x3f), argument>>24, argument>>16, argument>>8, argument>>0 }; // commands are 5 bytes long, plus 1 bytes of CRC (see section 7.3.1.1)
uint8_t crc7 = 0x00; // CRC-7 checksum for command message
// send command
for (uint8_t i=0; i<LENGTH(command); i++) {
spi_send(SPI(FLASH_SDCARD_SPI), command[i]); // send data
crc7 = (crc7_table[((crc7<<1)^command[i])])&0x7f; // update checksum
}
spi_send(SPI(FLASH_SDCARD_SPI), (crc7<<1)+0x01); // send CRC value (see section 7.3.1.1)
}
/** transmit command token and receive response token
* @param[in] index command index
* @param[in] argument command argument
* @param[out] response response data to read (if no error occurred)
* @param[in] size size of response to read
* @return response token R1 or 0xff if error occurred or card is not present
*/
static uint8_t flash_sdcard_command_response(uint8_t index, uint32_t argument, uint8_t* response, size_t size)
{
// send command token
gpio_clear(SPI_NSS_PORT(FLASH_SDCARD_SPI), SPI_NSS_PIN(FLASH_SDCARD_SPI)); // set CS low to select slave and start SPI mode (see section 7.2)
flash_sdcard_spi_wait(); // wait for N_CS (min. 0, but it works better with 8 clock cycles) before writing command (see section 7.5.1.1)
flash_sdcard_send_command(index, argument); // send command token
// get response token R1
uint8_t r1 = 0xff; // response token R1 (see section 7.3.2.1)
for (uint8_t i=0; i<8 && r1&0x80; i++) { // wait for N_CR (1 to 8 8 clock cycles) before reading response (see section 7.5.1.1)
r1 = flash_sdcard_spi_read(); // get response (see section 7.3.2.1)
}
if (0x00==(r1&0xfe) && 0!=size && NULL!=response) { // we have to read a response
for (size_t i=0; i<size; i++) {
response[i] = flash_sdcard_spi_read(); // get byte
}
}
// end communication
while (SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_BSY); // wait until not busy (= transmission completed)
// wait for N_EC (min. 0) before closing communication (see section 7.5.1.1)
gpio_set(SPI_NSS_PORT(FLASH_SDCARD_SPI), SPI_NSS_PIN(FLASH_SDCARD_SPI)); // set CS high to unselect card
// wait for N_DS (min. 0) before allowing any further communication (see section 7.5.1.1)
return r1;
}
/** read a data block
* @param[out] data data block to read (if no error occurred)
* @param[in] size size of response to read (a multiple of 2)
* @return 0 if succeeded, else control token (0xff for other errors)
*/
static uint8_t flash_sdcard_read_block(uint8_t* data, size_t size)
{
if (size%2 || 0==size || NULL==data) { // can't (and shouldn't) read odd number of bytes
return 0xff;
}
uint8_t token = 0xff; // to save the control block token (see section 7.3.3)
for (uint32_t i=0; i<n_ac && token==0xff; i++) { // wait for N_AC before reading data block (see section 7.5.2.1)
token = flash_sdcard_spi_read(); // get control token (see section 7.3.3)
}
if (0==(token&0xf0)) { // data error token received (see section 7.3.3.3)
if (0==(token&0x0f)) { // unknown error
token = 0xff;
}
} else if (0xfe==token) { // start block token received (see section 7.3.3.2)
// switch to 16-bits SPI data frame so we can use use built-in CRC-16
while (!(SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_TXE)); // wait until the end of any transmission
while (SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_BSY); // wait until not busy before disabling
spi_disable(SPI(FLASH_SDCARD_SPI)); // disable SPI to change format
spi_set_dff_16bit(SPI(FLASH_SDCARD_SPI)); // set SPI frame to 16 bits
SPI_CRC_PR(FLASH_SDCARD_SPI) = 0x1021; // set CRC-16-CCITT polynomial for data blocks (x^16+x^12+x^5+1) (see section 7.2.3)
spi_enable_crc(SPI(FLASH_SDCARD_SPI)); // enable and clear CRC
spi_enable(SPI(FLASH_SDCARD_SPI)); // enable SPI back
// get block data (ideally use DMA, but switching makes it more complex and this part doesn't take too much time)
for (size_t i=0; i<size/2; i++) {
uint16_t word = flash_sdcard_spi_read(); // get word
data[i*2+0] = (word>>8); // save byte
data[i*2+1] = (word>>0); // save byte
}
flash_sdcard_spi_read(); // read CRC (the CRC after the data block should clear the computed CRC)
if (SPI_CRC_RXR(FLASH_SDCARD_SPI)) { // CRC is wrong
token = 0xff;
} else { // no error occurred
token = 0;
}
// switch back to 8-bit SPI frames
while (!(SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_TXE)); // wait until the end of any transmission
while (SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_BSY); // wait until not busy before disabling
spi_disable(SPI(FLASH_SDCARD_SPI)); // disable SPI to change format
spi_disable_crc(SPI(FLASH_SDCARD_SPI)); // disable CRC since we don't use it anymore (and this allows us to clear the CRC next time we use it)
spi_set_dff_8bit(SPI(FLASH_SDCARD_SPI)); // set SPI frame to 8 bits
spi_enable(SPI(FLASH_SDCARD_SPI)); // enable SPI back
} else { // start block token not received
token = 0xff;
}
return token;
}
/** write a data block
* @param[in] data data block to write
* @param[in] size size of response to read (a multiple of 2)
* @return data response token (0xff for other errors)
*/
static uint8_t flash_sdcard_write_block(uint8_t* data, size_t size)
{
if (size%2 || 0==size || NULL==data) { // can't (and shouldn't) read odd number of bytes
return 0xff;
}
spi_send(SPI(FLASH_SDCARD_SPI), 0xfe); // send start block token (see section 7.3.3.2)
// switch to 16-bits SPI data frame so we can use use built-in CRC-16
while (!(SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_TXE)); // wait until the end of any transmission
while (SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_BSY); // wait until not busy before disabling
spi_disable(SPI(FLASH_SDCARD_SPI)); // disable SPI to change format
spi_set_dff_16bit(SPI(FLASH_SDCARD_SPI)); // set SPI frame to 16 bits
SPI_CRC_PR(FLASH_SDCARD_SPI) = 0x1021; // set CRC-16-CCITT polynomial for data blocks (x^16+x^12+x^5+1) (see section 7.2.3)
spi_enable_crc(SPI(FLASH_SDCARD_SPI)); // enable and clear CRC
spi_enable(SPI(FLASH_SDCARD_SPI)); // enable SPI back
// send block data (ideally use DMA, but switching makes it more complex and this part doesn't take too much time)
for (size_t i=0; i<size/2; i++) {
uint16_t word = (data[i*2+0]<<8)+data[i*2+1]; // prepare SPI frame
spi_send(SPI(FLASH_SDCARD_SPI), word); // senf data frame
}
spi_set_next_tx_from_crc(SPI(FLASH_SDCARD_SPI)); // send CRC
// switch back to 8-bit SPI frames
while (!(SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_TXE)); // wait until the end of any transmission
while (SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_BSY); // wait until not busy before disabling
spi_disable(SPI(FLASH_SDCARD_SPI)); // disable SPI to change format
spi_set_next_tx_from_buffer(SPI(FLASH_SDCARD_SPI)); // don't send CRC
spi_disable_crc(SPI(FLASH_SDCARD_SPI)); // disable CRC since we don't use it anymore (and this allows us to clear the CRC next time we use it)
spi_set_dff_8bit(SPI(FLASH_SDCARD_SPI)); // set SPI frame to 8 bits
spi_enable(SPI(FLASH_SDCARD_SPI)); // enable SPI back
uint8_t token = 0xff;
while (0x01!=(token&0x11)) {
token = flash_sdcard_spi_read(); // get data response token (see section 7.3.3.1)
}
while (0==flash_sdcard_spi_read()); // wait N_EC while the card is busy programming the data
return token;
}
/** get card status
* @param[out] status SD status (512 bits)
* @return response token R2 or 0xffff if error occurred or card is not present
*/
static uint16_t flash_sdcard_status(uint8_t* status)
{
// send CMD55 (APP_CMD) to issue following application command (see table 7-4)
uint8_t r1 = flash_sdcard_command_response(55, 0, NULL, 0); // (see table 7-3)
if ((r1&0xfe)) { // error occurred, not in idle state
return false;
}
// send ACMD13 command
gpio_clear(SPI_NSS_PORT(FLASH_SDCARD_SPI), SPI_NSS_PIN(FLASH_SDCARD_SPI)); // set CS low to select slave and start SPI mode (see section 7.2)
flash_sdcard_spi_wait(); // wait for N_CS (min. 0, but it works better with 8 clock cycles) before writing command (see section 7.5.2.1)
flash_sdcard_send_command(13, 0); // send ACMD13 (SD_STATUS) (see table 7-4)
// get response token R2
uint16_t r2 = 0xffff; // response token R2 (see section 7.3.2.3)
for (uint8_t i=0; i<8 && r2&0x8000; i++) { // wait for N_CR (1 to 8 8 clock cycles) before reading response (see section 7.5.1.1)
r2 = (flash_sdcard_spi_read()<<8); // get first byte of response (see section 7.3.2.1)
}
if (0==(r2&0x8000)) { // got the first byte
r2 += flash_sdcard_spi_read(); // read second byte (see 7.3.2.3)
}
// get data block
if (0==r2) { // no error
if (flash_sdcard_read_block(status, 64)) { // read 512 bits data block containing SD status
r2 |= (1<<11); // set communication error
}
}
// end communication
while (SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_BSY); // wait until not busy (= transmission completed)
// wait for N_EC (min. 0) before closing communication (see section 7.5.1.1)
gpio_set(SPI_NSS_PORT(FLASH_SDCARD_SPI), SPI_NSS_PIN(FLASH_SDCARD_SPI)); // set CS high to unselect card
// wait for N_DS (min. 0) before allowing any further communication (see section 7.5.1.1)
return r2;
}
/** transmit command token, receive response token and data block
* @param[in] index command index
* @param[in] argument command argument
* @param[out] data data block to read (if no error occurred)
* @param[in] size size of data to read (a multiple of 2)
* @return response token R1 or 0xff if error occurred or card is not present
*/
static uint8_t flash_sdcard_data_read(uint8_t index, uint32_t argument, uint8_t* data, size_t size)
{
if (size%2 || 0==size || NULL==data) { // can't (and shouldn't) read odd number of bytes
return 0xff;
}
// send command token
gpio_clear(SPI_NSS_PORT(FLASH_SDCARD_SPI), SPI_NSS_PIN(FLASH_SDCARD_SPI)); // set CS low to select slave and start SPI mode (see section 7.2)
flash_sdcard_spi_wait(); // wait for N_CS (min. 0, but it works better with 8 clock cycles) before writing command (see section 7.5.2.1)
flash_sdcard_send_command(index, argument); // send command token
// get response token R1
uint8_t r1 = 0xff; // response token R1 (see section 7.3.2.1)
for (uint8_t i=0; i<8 && r1&0x80; i++) { // wait for N_CR (1 to 8 8 clock cycles) before reading response (see section 7.5.1.1)
r1 = flash_sdcard_spi_read(); // get response (see section 7.3.2.1)
}
// get data block
if (0x00==r1) { // we can read a data block
if (flash_sdcard_read_block(data, size)) { // read data block
r1 |= (1<<3); // set communication error
}
}
// end communication
while (SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_BSY); // wait until not busy (= transmission completed)
// wait for N_EC (min. 0) before closing communication (see section 7.5.1.1)
gpio_set(SPI_NSS_PORT(FLASH_SDCARD_SPI), SPI_NSS_PIN(FLASH_SDCARD_SPI)); // set CS high to unselect card
// wait for N_DS (min. 0) before allowing any further communication (see section 7.5.1.1)
return r1;
}
/** transmit command token, receive response token and write data block
* @param[in] index command index
* @param[in] argument command argument
* @param[out] data data block to write
* @param[in] size size of data to write (a multiple of 2)
* @return data response token, or 0xff if error occurred or card is not present
* @note at the end of a write operation the SD status should be check to ensure no error occurred during programming
*/
static uint8_t flash_sdcard_data_write(uint8_t index, uint32_t argument, uint8_t* data, size_t size)
{
if (size%2 || 0==size || NULL==data) { // can't (and shouldn't) write odd number of bytes
return 0xff;
}
// send command token
gpio_clear(SPI_NSS_PORT(FLASH_SDCARD_SPI), SPI_NSS_PIN(FLASH_SDCARD_SPI)); // set CS low to select slave and start SPI mode (see section 7.2)
flash_sdcard_spi_wait(); // wait for N_CS (min. 0, but it works better with 8 clock cycles) before writing command (see section 7.5.2.1)
flash_sdcard_send_command(index, argument); // send command token
// get response token R1
uint8_t r1 = 0xff; // response token R1 (see section 7.3.2.1)
for (uint8_t i=0; i<8 && r1&0x80; i++) { // wait for N_CR (1 to 8 8 clock cycles) before reading response (see section 7.5.1.1)
r1 = flash_sdcard_spi_read(); // get response (see section 7.3.2.1)
}
// write data block
uint8_t drt = 0xff; // data response token (see section 7.3.3.1)
if (0x00==r1) { // we have to write the data block
drt = flash_sdcard_write_block(data, size); // write data block
}
// end communication
while (SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_BSY); // wait until not busy (= transmission completed)
// wait for N_EC (min. 0) before closing communication (see section 7.5.1.1)
gpio_set(SPI_NSS_PORT(FLASH_SDCARD_SPI), SPI_NSS_PIN(FLASH_SDCARD_SPI)); // set CS high to unselect card
// wait for N_DS (min. 0) before allowing any further communication (see section 7.5.1.1)
return drt;
}
bool flash_sdcard_setup(void)
{
// reset values
initialized = false;
n_ac = 8;
sdcard_size = 0;
erase_size = 0;
// check if card is present
if (!flash_sdcard_card_detect()) {
return false;
}
// configure SPI peripheral
rcc_periph_clock_enable(RCC_SPI_SCK_PORT(FLASH_SDCARD_SPI)); // enable clock for GPIO peripheral for clock signal
gpio_set_mode(SPI_SCK_PORT(FLASH_SDCARD_SPI), GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, SPI_SCK_PIN(FLASH_SDCARD_SPI)); // set SCK as output (clock speed will be negotiated later)
rcc_periph_clock_enable(RCC_SPI_MOSI_PORT(FLASH_SDCARD_SPI)); // enable clock for GPIO peripheral for MOSI signal
gpio_set_mode(SPI_MOSI_PORT(FLASH_SDCARD_SPI), GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, SPI_MOSI_PIN(FLASH_SDCARD_SPI)); // set MOSI as output
rcc_periph_clock_enable(RCC_SPI_MISO_PORT(FLASH_SDCARD_SPI)); // enable clock for GPIO peripheral for MISO signal
gpio_set_mode(SPI_MISO_PORT(FLASH_SDCARD_SPI), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, SPI_MISO_PIN(FLASH_SDCARD_SPI)); // set MISO as input
gpio_set(SPI_MISO_PORT(FLASH_SDCARD_SPI), SPI_MISO_PIN(FLASH_SDCARD_SPI)); // pull pin high to detect when the card is not answering (or not present) since responses always start with MSb 0
rcc_periph_clock_enable(RCC_SPI_NSS_PORT(FLASH_SDCARD_SPI)); // enable clock for GPIO peripheral for NSS (CS) signal
gpio_set_mode(SPI_NSS_PORT(FLASH_SDCARD_SPI), GPIO_MODE_OUTPUT_10_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, SPI_NSS_PIN(FLASH_SDCARD_SPI)); // set NSS (CS) as output
rcc_periph_clock_enable(RCC_AFIO); // enable clock for SPI alternate function
rcc_periph_clock_enable(RCC_SPI(FLASH_SDCARD_SPI)); // enable clock for SPI peripheral
spi_reset(SPI(FLASH_SDCARD_SPI)); // clear SPI values to default
spi_init_master(SPI(FLASH_SDCARD_SPI), SPI_CR1_BAUDRATE_FPCLK_DIV_256, SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, SPI_CR1_CPHA_CLK_TRANSITION_1, SPI_CR1_DFF_8BIT, SPI_CR1_MSBFIRST); // initialise SPI as master, divide clock by 256 (72E6/256=281 kHz) since maximum SD card clock frequency (fOD, see section 7.8/6.6.6) during initial card-identification mode is 400 kHz (maximum SPI PCLK clock is 72 Mhz, depending on which SPI is used), set clock polarity to idle low (not that important), set clock phase to do bit change on falling edge (from SD card spec, polarity depends on clock phase), use 8 bits frames (as per spec), use MSb first
spi_set_full_duplex_mode(SPI(FLASH_SDCARD_SPI)); // ensure we are in full duplex mode
spi_enable_software_slave_management(SPI(FLASH_SDCARD_SPI)); // control NSS (CS) manually
spi_set_nss_high(SPI(FLASH_SDCARD_SPI)); // set NSS high (internally) so we can output
spi_disable_ss_output(SPI(FLASH_SDCARD_SPI)); // disable NSS output since we control CS manually
gpio_set(SPI_NSS_PORT(FLASH_SDCARD_SPI), SPI_NSS_PIN(FLASH_SDCARD_SPI)); // set CS high to unselect card
// sadly we can't use the interrupts as events to sleep (WFE) since sleep disables also communication (e.g. going to sleep until Rx buffer is not empty prevents transmission)
spi_enable(SPI(FLASH_SDCARD_SPI)); // enable SPI
// start card-identification (see section 7.2.1/4.2)
uint8_t r1 = 0;
// send CMD0 (GO_IDLE_START) to start the card identification (see section 7.2.1)
r1 = flash_sdcard_command_response(0, 0, NULL, 0); // (see table 7-3)
if (0x01!=r1) { // error occurred, not in idle state
return false;
}
// send CMD8 (SEND_IF_COND) to inform about voltage (1: 2.7-3.6V, aa: recommended check pattern) (see section 7.2.1)
uint8_t r7[4] = {0}; // to store response toke R7 (see section 7.3.2.6)
r1 = flash_sdcard_command_response(8, 0x000001aa, r7, sizeof(r7)); // (see table 7-3)
if (0x01==r1) { // command supported, in idle state
if (!(r7[2]&0x1)) { // 2.7-3.6V not supported (see table 5-1)
return false;
} else if (0xaa!=r7[3]) { // recommended pattern not returned (see section 4.3.13)
return false;
}
} else if (0x05!=r1) { // illegal command (cards < physical spec v2.0 don't support CMD8) (see section 7.2.1)
return false;
}
// send CMD58 (READ_OCR) to read Operation Conditions Register (see section 7.2.1)
uint8_t r3[4] = {0}; // to store response token R3 (see section 7.3.2.4)
r1 = flash_sdcard_command_response(58, 0, r3, sizeof(r3)); // (see table 7-3)
if (0x01!=r1) { // error occurred, not in idle state
return false;
} else if (!(r3[1]&0x30)) { // 3.3V not supported (see table 5-1)
return false;
}
do {
// send CMD55 (APP_CMD) to issue following application command (see table 7-4)
r1 = flash_sdcard_command_response(55, 0, NULL, 0); // (see table 7-3)
if (0x01!=r1) { // error occurred, not in idle state
return false;
}
// send ACMD41 (SD_SEND_OP_COND) with Host Capacity Support (0b: SDSC Only Host, 1b: SDHC or SDXC Supported) (see section 7.2.1)
r1 = flash_sdcard_command_response(41, 0x40000000, NULL, 0); // (see table 7-4)
if (r1&0xfe) { // error occurred
return false;
}
} while (0x00!=r1); // wait until card is ready (see section 7.2.1)
// send CMD58 (READ_OCR) to read Card Capacity Status (CCS) (see section 7.2.1)
r1 = flash_sdcard_command_response(58, 0, r3, sizeof(r3)); // (see table 7-3)
if (r1) { // error occurred
return false;
}
// card power up status bit (bit 31) is set when power up is complete (see table 5-1)
if (0x00==(r3[0]&0x80)) {
return false;
}
sdsc = (0==(r3[0]&0x40)); // CCS is bit 30 in OCR (see table 5-1)
// now the card identification is complete and we should be in data-transfer mode (see figure 7-1)
// we can switch clock frequency to fPP (max. 25 MHz) (see section 4.3/6.6.6)
while (!(SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_TXE)); // wait until the end of any transmission
while (SPI_SR(SPI(FLASH_SDCARD_SPI))&SPI_SR_BSY); // wait until not busy before disabling
spi_disable(SPI(FLASH_SDCARD_SPI)); // disable SPI to change clock speed
spi_set_baudrate_prescaler(SPI(FLASH_SDCARD_SPI), SPI_CR1_BR_FPCLK_DIV_4); // set clock speed to 18 MHz (72/4=18, < 25 MHz)
spi_enable(SPI(FLASH_SDCARD_SPI)); // enable SPI back
// send CMD9 (SEND_CSD) to get Card Specific Data (CSD) and calculate N_AC (see section 7.2.6)
uint8_t csd[16] = {0}; // CSD response (see chapter 7.2.6)
r1 = flash_sdcard_data_read(9, 0, csd, sizeof(csd)); // (see table 7-3)
if (r1) { // error occurred
return false;