aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKing Kévin <kingkevin@cuvoodoo.info>2020-02-19 21:40:03 +0100
committerKing Kévin <kingkevin@cuvoodoo.info>2020-02-19 21:40:03 +0100
commitcc75867942155c9f9a1571f54234b58d4eedb64f (patch)
tree42e9c4fb582c4a52e3f3cd0aa7f6dd1e21309bbf
parenta5a0ae84d1e5358f6f1da712178fc0e5b02ced1d (diff)
application: complete enforecer application, and documentation
-rw-r--r--README.md90
-rw-r--r--application.c292
2 files changed, 328 insertions, 54 deletions
diff --git a/README.md b/README.md
index f37860c..21906c0 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
-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 sound lever enforcer.
+It will cut the power of speakers when it's too loud.
project
=======
@@ -6,48 +7,75 @@ project
summary
-------
-*describe project purpose*
+The sound level enforcer receives measurements from a sound level meter over Bluetooth.
+The sound level is then show on a display (0 if it is not connected or receives no data).
+If this level is below the threshold configured, the display is dim.
+If the level is above the threshold configured, the brightness is higher.
+If the level is above the threshold configured, for a set amount of time, a relay it activated.
+The relay's purpose is to cut the power of speakers/mixer.
+"too loud" will be shown on the display, and the button will light up.
+Press the button to reset the device.
+The relay will be deactivated, and the cycle starts again.
+
+When powered up or reset, the display will show the sound level threshold (followed by the unit "dBa"), and duration (followed by the unit "dBa").
+To change the sound level threshold, enter "threshold xxx" on the serial interface (over USB, or UART).
+To change the sound level duration, enter "duration xxx" on the serial interface (over USB, or UART).
technology
----------
-*described electronic details*
-
-board
-=====
+The firmware runs on a [black pill](https://wiki.cuvoodoo.info/doku.php?id=stm32f1xx#black_pill) development board.
+It is based on a STM32F103C8T6 micro-controller.
+This will do all the processing and control the peripherals.
-The current implementation uses a [core board](https://wiki.cuvoodoo.info/doku.php?id=stm32f1xx#core_board).
+The Bluetooth module is a HC-05.
+It should be configured to automatically connect to the sound level meter.
+It should receive the sound level over Bluetooth in the format "123.4 dBa\n".
+The measurement will be forwarded to the micro-controller over the UART part configured at 115200 bps 8N1.
-The underlying template also supports following board:
+This does not use a Bluetooth Low Energy (BLE) module because it does not fit the needs.
+There is no need to save energy since the device needs permanent power just for the display.
+There is a constant stream of data (not fitting BLE principles).
+The is a specified Classic Bluetooth profile for serial data: Serial Port Profile (SPP).
+BLE module use non-standard GATT characteristics for serial data transfer.
-- [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
+The display is a 4-digit 7-segment LED display.
+This is controlled by a TM1367.
-**Which board is used is defined in the Makefile**.
-This is required to map the user LED and button provided on the board
+The relay is a SRD-05VDC-SL-C module (250V 10A).
+Connect it to the device using a 2.5 mm jack.
-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.
+The momentary button should have a built-in LED.
connections
===========
-Connect the peripherals the following way (STM32F10X signal; STM32F10X pin; peripheral pin; peripheral signal; comment):
-
-- *list board to peripheral pin connections*
+Connect the peripherals as described below.
+
+HC-05 Bluetooth SPP module:
+- STATE: no connect
+- RX: no connect
+- TX: USART3_RX/PB11
+- GND: ground
+- VCC: 5V
+- EN: no connect
+
+button (momentary, with LED)
+- +: 5V
+- -: 470 Ohm resistor to PB12 (sometimes the resistor is already built in the button)
+- S: GND
+- S: NRST
+
+7-segment display TM1637:
+- GND: ground
+- VCC: 5V
+- DIO: PB13
+- CLK: PB14
+
+relay, connected to 2.5 mm TRS jack:
+- tip, VCC: 5V
+- ring, IN: PB15
+- sleeve, GND: ground
All pins are configured using `define`s in the corresponding source code.
@@ -83,7 +111,7 @@ It is up to the application to advertise USB DFU support (i.e. as does the provi
The `bootlaoder` 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`.
diff --git a/application.c b/application.c
index 089b264..fc610ce 100644
--- a/application.c
+++ b/application.c
@@ -12,7 +12,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-/** STM32F1 application example
+/** sound level enforcer
* @file
* @author King Kévin <kingkevin@cuvoodoo.info>
* @date 2016-2020
@@ -23,7 +23,8 @@
#include <stdlib.h> // standard utilities
#include <string.h> // string utilities
#include <time.h> // date/time utilities
-#include <ctype.h> // utilities to check chars
+#include <ctype.h> // utilities to check char
+#include <math.h> // for rounding floats
/* STM32 (including CM3) libraries */
#include <libopencmsis/core_cm3.h> // Cortex M3 utilities
@@ -48,6 +49,8 @@
#include "terminal.h" // handle the terminal interface
#include "menu.h" // menu utilities
#include "led_tm1637.h" // 7-segment display
+#include "spp_rx.h" // Bluetooth SPP input
+#include "flash_internal.h" // settings storage
/** watchdog period in ms */
#define WATCHDOG_PERIOD 10000
@@ -75,6 +78,25 @@ static time_t time_start = 0;
volatile bool rtc_internal_tick_flag = false; /**< flag set when internal RTC ticked */
/** @} */
+/** buffer for the data received over Bluetooth from the sound level meter */
+static char spp_input_buffer[16];
+/** how much received data is buffered */
+static uint8_t spp_input_i = 0;
+
+/** pin to control relay (active low) */
+#define RELAY_PIN PB15
+
+/** default sound level threshold (in dBa) */
+#define SOUND_LEVEL_THRESHOLD 100
+/** configured sound level threshold (in dBa) */
+static uint8_t sound_level_threshold = SOUND_LEVEL_THRESHOLD;
+/** default sound level duration (in seconds) */
+#define SOND_LEVEL_DURATION 10
+/** configured sound level duration (in seconds) */
+static uint8_t sound_level_duration = SOND_LEVEL_DURATION;
+/** if we show the received sound level value */
+static bool sound_level_show = false;
+
size_t putc(char c)
{
size_t length = 0; // number of characters printed
@@ -129,6 +151,90 @@ static void command_reset(void* argument);
*/
static void command_bootloader(void* argument);
+/** load sound level threshold and duration settings from flash
+ * @return if values are loaded from flash (else set to default)
+ */
+static bool sound_level_load(void)
+{
+ uint8_t eeprom_data[4] = {0};
+ const bool eeprom_read = flash_internal_eeprom_read(eeprom_data, sizeof(eeprom_data));
+ if (eeprom_read) {
+ eeprom_data[1] ^= 0xff; // xor value (sort of checksum)
+ eeprom_data[3] ^= 0xff; // xor value (sort of checksum)
+ if (eeprom_data[0] == eeprom_data[1] && eeprom_data[2] == eeprom_data[3]) {
+ sound_level_threshold = eeprom_data[0];
+ sound_level_duration = eeprom_data[2];
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ sound_level_threshold = SOUND_LEVEL_THRESHOLD;
+ sound_level_duration = SOND_LEVEL_DURATION;
+ return false;
+ }
+}
+
+/** save sound level threshold and duration settings into flash
+ * @return if succeeded
+ */
+static bool sound_level_save(void)
+{
+ const uint8_t eeprom_data[4] = {
+ sound_level_threshold,
+ sound_level_threshold ^ 0xff,
+ sound_level_duration,
+ sound_level_duration ^ 0xff,
+ };
+ const int32_t rc = flash_internal_eeprom_write(eeprom_data, sizeof(eeprom_data));
+ if (rc < 0) {
+ printf("error saving sound level data: %d\n", rc);
+ }
+
+ return rc == sizeof(eeprom_data);
+}
+
+/** set sound level threshold
+ * @param[in] argument sound level threshold
+ */
+static void command_sound_level_threshold(void* argument)
+{
+ if (argument) { // tachometer value has been provided
+ const uint32_t value = *(uint32_t*)argument; // get target sound level threshold value
+ sound_level_threshold = value;
+ if (!sound_level_save()) {
+ puts("could not save sound level threshold\n");
+ }
+ }
+ printf("sound level threshold set to %u dBa\n", sound_level_threshold);
+}
+
+/** set sound level duration
+ * @param[in] argument sound level duration
+ */
+static void command_sound_level_duration(void* argument)
+{
+ if (argument) { // tachometer value has been provided
+ const uint32_t value = *(uint32_t*)argument; // get target sound level duration value
+ sound_level_duration = value;
+ if (!sound_level_save()) {
+ puts("could not save sound level duration\n");
+ }
+ }
+ printf("sound level duration set to %u s\n", sound_level_duration);
+}
+
+/** show/hide received sound level
+ * @param[in] argument not used
+ */
+static void command_show(void* argument)
+{
+ (void)argument; // we won't use the argument
+ sound_level_show = !sound_level_show; // toggle setting
+ puts(sound_level_show ? "show" : "hide");
+ puts(" received sound level\n");
+}
+
/** list of all supported commands */
static const struct menu_command_t menu_commands[] = {
{
@@ -157,7 +263,7 @@ static const struct menu_command_t menu_commands[] = {
},
#if RTC_DATE_TIME
{
- .shortcut = 'd',
+ .shortcut = 'D',
.name = "date",
.command_description = "show/set date and time",
.argument = MENU_ARGUMENT_STRING,
@@ -181,6 +287,30 @@ static const struct menu_command_t menu_commands[] = {
.argument_description = NULL,
.command_handler = &command_bootloader,
},
+ {
+ .shortcut = 't',
+ .name = "threshold",
+ .command_description = "get/set sound level threshold (in dBa)",
+ .argument = MENU_ARGUMENT_UNSIGNED,
+ .argument_description = "[value]",
+ .command_handler = &command_sound_level_threshold,
+ },
+ {
+ .shortcut = 'd',
+ .name = "duration",
+ .command_description = "get/set sound level duration (in seconds)",
+ .argument = MENU_ARGUMENT_UNSIGNED,
+ .argument_description = "[value]",
+ .command_handler = &command_sound_level_duration,
+ },
+ {
+ .shortcut = 's',
+ .name = "show",
+ .command_description = "show/hide received sound level",
+ .argument = MENU_ARGUMENT_NONE,
+ .argument_description = NULL,
+ .command_handler = &command_show,
+ },
};
static void command_help(void* argument)
@@ -200,34 +330,34 @@ static void command_version(void* argument)
// 0x414: high-density, 256-512 kB flash
// 0x430: XL-density, 768-1024 kB flash
// 0x418: connectivity
- puts("device family: ");
+ printf("device family: ");
switch (DBGMCU_IDCODE & DBGMCU_IDCODE_DEV_ID_MASK) {
case 0: // this is a known issue document in STM32F10xxC/D/E Errata sheet, without workaround
- puts("unreadable\n");
+ printf("unreadable\n");
break;
case 0x412:
- puts("low-density\n");
+ printf("low-density\n");
break;
case 0x410:
- puts("medium-density\n");
+ printf("medium-density\n");
break;
case 0x414:
- puts("high-density\n");
+ printf("high-density\n");
break;
case 0x430:
- puts("XL-density\n");
+ printf("XL-density\n");
break;
case 0x418:
- puts("connectivity\n");
+ printf("connectivity\n");
break;
default:
- puts("unknown\n");
+ printf("unknown\n");
break;
}
// show flash size
- puts("flash size: ");
+ printf("flash size: ");
if (0xffff == DESIG_FLASH_SIZE) {
- puts("unknown (probably a defective micro-controller\n");
+ printf("unknown (probably a defective micro-controller\n");
} else {
printf("%u KB\n", DESIG_FLASH_SIZE);
}
@@ -316,6 +446,36 @@ static void process_command(char* str)
}
}
+/** parse and display sound level value
+ * @param[io] str line with dBA measurement
+ * @return parsed measurement (0 if failed)
+ * @warning modifies str
+ */
+static uint8_t parse_measurement(char* str)
+{
+ if (strlen(str) < 5) { // minimum size for a valid message
+ return 0;
+ }
+ const char* delimiter = " "; // words are separated by spaces
+ const char* value_s = strtok(str, delimiter); // get measurement value
+ if (!value_s) {
+ return 0;
+ }
+ const char* unit = strtok(NULL, delimiter); // get measurement unit
+ if (!unit) {
+ return 0;
+ }
+ if (strncmp("dBa", unit, 3)) { // the measurement unit is the right one
+ return 0;
+ }
+ const double value_f = atof(value_s); // get measurement value
+ if (isnan(value_f) || value_f < 0.1) {
+ return 0;
+ }
+ const uint8_t value_i = round(value_f); // get rounded measurement value
+ return value_i;
+}
+
/** program entry point
* this is the firmware function started by the micro-controller
*/
@@ -342,7 +502,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 sound lever meter display\n"); // print welcome message
+ puts("\nwelcome to the CuVoodoo sound lever enforcer\n"); // print welcome message
#if DEBUG
// show reset cause
@@ -395,13 +555,49 @@ void main(void)
time_start = rtc_get_counter_val(); // get start time from internal RTC
puts("OK\n");
- // setup display
- printf("setup 7-segment display: ");
+ puts("setup relay: ");
+ gpio_set(GPIO_PORT(RELAY_PIN), GPIO_PIN(RELAY_PIN)); // idle not activated
+ gpio_set_mode(GPIO_PORT(RELAY_PIN), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO_PIN(RELAY_PIN)); // set pin as output (sink to enable)
+ puts("OK\n");
+
+ puts("setup 7-segment display: ");
led_tm1637_setup();
- led_tm1637_brightness(LED_TM1637_14DIV16);
- led_tm1637_time(88, 88);
+ led_tm1637_brightness(LED_TM1637_14DIV16); // set maximum brightness
+ led_tm1637_time(88, 88); // show test pattern
led_tm1637_on();
- printf("OK\n");
+ puts("OK\n");
+
+ puts("setup SPP receiver: ");
+ spp_rx_setup();
+ puts("OK\n");
+
+ puts("read sound level settings: ");
+ flash_internal_eeprom_setup(1); // dedicate one page to store the settings
+ if (sound_level_load()) {
+ puts("set");
+ } else {
+ puts("default");
+ }
+ puts(" values\n");
+ command_sound_level_threshold(NULL); // show value
+ command_sound_level_duration(NULL); // show value
+
+ // show sound level
+ iwdg_reset(); // kick the dog
+ led_tm1637_number(sound_level_threshold, false); // display threshold
+ sleep_ms(1000); // wait some time for the user to read
+ iwdg_reset(); // kick the dog
+ led_tm1637_text(" dBa"); // display unit
+ sleep_ms(1000); // wait some time for the user to read
+ iwdg_reset(); // kick the dog
+ led_tm1637_number(sound_level_duration, false); // display duration
+ sleep_ms(1000); // wait some time for the user to read
+ iwdg_reset(); // kick the dog
+ led_tm1637_text(" sec"); // display unit
+ sleep_ms(1000); // wait some time for the user to read
+ iwdg_reset(); // kick the dog
+ led_tm1637_brightness(LED_TM1637_1DIV16); // set minimum brightness
+ led_tm1637_number(0, false); // show measurement
// setup terminal
terminal_prefix = ""; // set default prefix
@@ -411,11 +607,15 @@ 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
+ uint32_t sound_level_valid = 0; // last time a valid value has been received
+ uint32_t sound_level_under = rtc_get_counter_val(); // last time the threshold has been exceeded
+ bool sound_level_invalid = false; // no value has been received for some time
+ bool sound_level_exceeded = false; // the level has exceeded the threshold longer than the duration
while (true) { // infinite loop
iwdg_reset(); // kick the dog
if (user_input_available) { // user input is available
action = true; // action has been performed
- led_toggle(); // toggle LED
+ //led_toggle(); // show activity
char c = user_input_get(); // store receive character
terminal_send(c); // send received character to terminal
}
@@ -423,11 +623,57 @@ void main(void)
rtc_internal_tick_flag = false; // reset flag
action = true; // action has been performed
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)
- uint32_t seconds = rtc_get_counter_val() / RTC_TICKS_SECOND; // get number of seconds
- led_tm1637_time(seconds / 60, seconds % 60); // display time
+ //led_toggle(); // heartbeat
+ if (sound_level_exceeded) {
+ led_tm1637_brightness(LED_TM1637_14DIV16); // set maximum brightness
+ if ((rtc_get_counter_val() / RTC_TICKS_SECOND) % 2) {
+ led_tm1637_text("too ");
+ } else {
+ led_tm1637_text("loud");
+ }
+ } else if (!sound_level_invalid && (((rtc_get_counter_val() - sound_level_valid) / RTC_TICKS_SECOND) >= 3)) { // no value received
+ sound_level_invalid = true; // remember the value is invalid
+ led_tm1637_number(0, false); // display empty number
+ led_tm1637_brightness(LED_TM1637_1DIV16); // set minimum brightness
+ } else if (!sound_level_exceeded && sound_level_valid > sound_level_under && (((rtc_get_counter_val() - sound_level_under) / RTC_TICKS_SECOND) >= sound_level_duration)) { // sound level exceeded for too long
+ sound_level_exceeded = true; // remember the value exceeded
+ led_on(); // light up reset switch
+ gpio_clear(GPIO_PORT(RELAY_PIN), GPIO_PIN(RELAY_PIN)); // enable relay
+ }
}
}
+ if (spp_rx_input_available) {
+ action = true; // action has been performed
+ //led_toggle(); // show activity
+ const char c = spp_rx_input_get(); // get the received data (also clears the flag)
+ if (!sound_level_exceeded) {
+ if (spp_input_i < LENGTH(spp_input_buffer) - 1) { // only store when there is enough space
+ spp_input_buffer[spp_input_i++] = c; // store received data
+ spp_input_buffer[spp_input_i] = '\0'; // end string
+ }
+ if ('\n' == c) { // end of line received
+ const uint8_t sound_level = parse_measurement(spp_input_buffer); // parse and display the received measurement
+ spp_input_i = 0; // reset buffer for next line
+ if (sound_level) {
+ sound_level_valid = rtc_get_counter_val(); // remember we got a valid value
+ if (sound_level_show) {
+ printf("%u dBa\n", sound_level);
+ }
+ led_tm1637_number(sound_level, false); // display number
+ if (sound_level_invalid) {
+ sound_level_invalid = false; // remember we got a value
+ sound_level_under = sound_level_valid; // remember the value is under the threshold
+ }
+ if (sound_level < sound_level_threshold) {
+ led_tm1637_brightness(LED_TM1637_1DIV16); // set minimum brightness
+ sound_level_under = sound_level_valid; // remember the value is under the threshold
+ } else {
+ led_tm1637_brightness(LED_TM1637_14DIV16); // set maximum brightness
+ }
+ }
+ } // end of line received
+ } // !sound_level_exceeded
+ } // spp_rx_input_available
if (action) { // go to sleep if nothing had to be done, else recheck for activity
action = false;
} else {