application: complete enforecer application, and documentation

This commit is contained in:
King Kévin 2020-02-19 21:40:03 +01:00
parent a5a0ae84d1
commit cc75867942
2 changed files with 327 additions and 53 deletions

View File

@ -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`.

View File

@ -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 {