From cc75867942155c9f9a1571f54234b58d4eedb64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?King=20K=C3=A9vin?= Date: Wed, 19 Feb 2020 21:40:03 +0100 Subject: [PATCH] application: complete enforecer application, and documentation --- README.md | 88 +++++++++------ application.c | 292 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 327 insertions(+), 53 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* +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. -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 current implementation uses a [core board](https://wiki.cuvoodoo.info/doku.php?id=stm32f1xx#core_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. -The underlying template also supports following board: +The display is a 4-digit 7-segment LED display. +This is controlled by a TM1367. -- [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 relay is a SRD-05VDC-SL-C module (250V 10A). +Connect it to the device using a 2.5 mm jack. -**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. +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): +Connect the peripherals as described below. -- *list board to peripheral pin connections* +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 . * */ -/** STM32F1 application example +/** sound level enforcer * @file * @author King Kévin * @date 2016-2020 @@ -23,7 +23,8 @@ #include // standard utilities #include // string utilities #include // date/time utilities -#include // utilities to check chars +#include // utilities to check char +#include // for rounding floats /* STM32 (including CM3) libraries */ #include // 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 {