Compare commits

...

9 Commits

2 changed files with 298 additions and 6 deletions

View File

@ -1,9 +1,48 @@
this is a template firmware for [ESP32-S2](https://www.espressif.com/en/products/socs/esp32-s2)-based micro-controller projects.
this project is about interfacing the GM-1352 sound level meter using an ESP32-S2-based board, so to read the sound level measurements digitally.
GM-1352
=======
the GM-1352 is the successor of the GM-1351, and very similar to it.
the main difference is that the power is not delivered by a 9V battery, but by 3x1.5V AA batteries.
[CuVoodoo #030](https://www.cuvoodoo.info/cuvoodoo-030-audio-is-killing-the-music/) already explained how to interface it.
basically just tap on the lines between the MCU (a EFM8BB10F8G-QFN20) and LCD controller (a Chip-on-Board under a blob of epoxy).
there are test points available for that:
- CS: chip select, to start data communication
- DATA: the actual data bits
- WR: the data clock (not periodic)
there are additional test points to read (or assert) button presses:
- 1: HOLD button (press low)
- 2: MIN/MAX button (press low)
- 3: POWER button (press low)
there is another test point marked S, but I don't know what this is for.
also note that the LCD has segments for Bluetooth and USB.
there might be variants of the GM-1352 with these options.
board
=====
the board used is a [WEMOS](https://www.wemos.cc/en/latest/index.html) [S2 mini](https://www.wemos.cc/en/latest/s2/s2_mini.html).
this repository is the firmware for [ESP32-S2](https://www.espressif.com/en/products/socs/esp32-s2)-based [WEMOS](https://www.wemos.cc/en/latest/index.html) [S2 mini](https://www.wemos.cc/en/latest/s2/s2_mini.html) board.
connect the GM-1352 to the S2 mini as follows:
- B+ to 5V: to power or get power from the device
- B- to GND: ground reference
- POWER to 16: to wake up the S2 when the device is powered (the S2 will shut down shortly after the GM-1352)
- DATA to 18: this will be the SPI MOSI line to read the measurments
- CS to 33: this will be the SPI CS line to identify transactions
- WR to 35: this will be the SPI SCK line to clock the data bits (not periodic)
I soldered wires to the pads, and broke them out on a 8-pin female header, located above the battery compartment.
this also to easily connect on row of the S2 mini to it on the back of the GM-1352.
now you can power the sound level meter through the S2 mini USB port, and read the measurements over its serial CDC ACM interface.
compile and flash
=================

View File

@ -3,11 +3,16 @@
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/reent.h>
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_sleep.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "tinyusb.h"
#include "tusb_cdc_acm.h"
#include "tusb_dfu_rt.h"
@ -15,14 +20,155 @@
#include "sdkconfig.h"
#include "driver/gpio.h"
#include "driver/spi_slave.h"
// get the length of an array
#define LENGTH(x) (sizeof(x) / sizeof((x)[0]))
// GPIO to force DFU mode (on high)
#define DFU_PIN 14
// GPIO for on-board LED (WEMOS S2 mini, source on)
#define LED_BOARD 15
// connect GPIO to test pad 3
#define GPIO_POWER (16U)
// connect GPIO to test pad DATA
#define GPIO_MOSI (18U)
// connect GPIO to test pad WR
#define GPIO_SCLK (35U)
// connect GPIO to test pad CS
#define GPIO_CS (33U)
static const char *TAG = "main";
/* the MCU sends 1 or 17 bytes frame the to LCD controller
* the below described which bit corresponds to which segment
* digit 1 is left most on the LCD, bit 7 is the most significant bit.
*
* byte 0:
* - 0x80: initialize LCD controller (1 byte frame)
* - 0xa0: set segments (17 bytes frame)
*
* byte 1:
* - bit 7: no segment
* - bit 6: no segment
* - bit 5: battery low level
* - bit 4: battery mid level
* - bit 3: battery high level
* - bit 2: OVER
* - bit 1: USB
* - bit 0: Bluetooth icon
*
* byte 2:
* - bit 7: battery case
* - bit 6: MAX
* - bit 5: MIN
* - bit 4: HOLD
* - bit 3: digit 1, segment b and segment c
* - bit 2: digit 2, segment f
* - bit 1: digit 2, segment g
* - bit 0: digit 2, segment e
*
* byte 3:
* - bit 7: FAST
* - bit 6: digit 2, segment a
* - bit 5: digit 2, segment b
* - bit 4: digit 2, segment c
* - bit 3: digit 2, segment d
* - bit 2: digit 3, segment f
* - bit 1: digit 3, segment g
* - bot 0: digit 3, segment e
*
* byte 4:
* - bit 7: SLOW
* - bit 6: digit 3, segment a
* - bit 5: digit 3, segment b
* - bit 4: digit 3, segment c
* - bit 3: digit 3, segment d
* - bit 2: no segment
* - bit 1: no segment
* - bit 0: digit 3, segment dp
*
* byte 5:
* - bit 7: dBC
* - bit 6: digit 4, segment f
* - bit 5: digit 4, segment g
* - bit 4: digit 4, segment e
* - bit 3: dBA
* - bit 2: digit 4, segment a
* - bit 1: digit 4, segment b
* - bit 0: digit 4, segment c
*
* byte 6:
* - bit 7: digit 4, segment d
*
* all other bits can be set to any value
*/
// number of bytes in an LCD frame
#define FRAME_SIZE (17U)
/** which bit should be set to enable which segment in specific digits
* @note 0 = byte 0 bit 0
*/
static const uint8_t digit_segments[4][8] = {
{ // digit 1
0, // a (segment does not exist)
16 + 3, // b
16 + 3, // c
0, // d (segment does not exist)
0, // e (segment does not exist)
0, // f (segment does not exist)
0, // g (segment does not exist)
0, // dp (segment does not exist)
},
{ // digit 2
24 + 6, // a
24 + 5, // b
24 + 4, // c
24 + 3, // d
16 + 0, // e
16 + 2, // f
16 + 1, // g
0, // dp (segment does not exist)
},
{ // digit 3
32 + 6, // a
32 + 5, // b
32 + 4, // c
32 + 3, // d
24 + 0, // e
24 + 2, // f
24 + 1, // g
0, // dp (actually 24 + 0 but we don't use it)
},
{ // digit 4
40 + 2, // a
40 + 1, // b
40 + 0, // c
48 + 7, // d
40 + 4, // e
40 + 6, // f
40 + 5, // g
0, // dp (segment does not exist)
},
};
/** the index corresponds to the digit to be displayed, the value represent the segments to be enabled
* @note LSb = a, MSb = dp
*/
static const uint8_t number_segments[10] = {
0x3f, // 0: a b c d e f
0x06, // 1: b c
0x5b, // 2: a b d e g
0x4f, // 3: a b c d g
0x66, // 4: b c f g
0x6d, // 5: a c d f g
0x7d, // 6: a c d e f g
0x07, // 7: a b c
0x7f, // 8: a b c d e f g
0x6f, // 9; a c b d f g
};
/**
* @brief USB string descriptor
*/
@ -31,26 +177,80 @@ static const char* usb_str_desc[USB_STRING_DESCRIPTOR_ARRAY_SIZE] = {
CONFIG_TINYUSB_DESC_MANUFACTURER_STRING, // 1: Manufacturer
CONFIG_TINYUSB_DESC_PRODUCT_STRING, // 2: Product
CONFIG_TINYUSB_DESC_SERIAL_STRING, // 3: Serials, should use chip ID
CONFIG_TINYUSB_DESC_CDC_STRING, // 4: CDC Interface
"GM1532 measurements", // 4: CDC Interface
"", // 5: MSC Interface
"DFU (runtime mode)" // 6: DFU RT
};
static QueueHandle_t meas_queue; // queue for the measurement messages received
#define MEAS_QUEUE_SIZE 4 // max number of control message received
/* handle measurement received */
static void meas_task(void *pvParameter)
{
spi_slave_transaction_t t;
while (xQueueReceive(meas_queue, &t, portMAX_DELAY) == pdTRUE) {
const uint8_t* rx_buffer = t.rx_buffer; // t.rx_buffer is type-less
if (t.trans_len > 6 && 0xa0 == rx_buffer[0]) { // frame long enough for the LCD data
// extract digit segments
uint8_t segments[4] = {0}; // which digit segments are enabled
for (uint8_t digit = 0; digit < LENGTH(segments); digit++) {
segments[digit] = 0; // clear all segments
for (uint8_t seg = 0; seg < 8; seg++) {
if (rx_buffer[digit_segments[digit][seg] / 8] & (1 << (digit_segments[digit][seg] % 8))) {
segments[digit] |= (1 << seg);
}
}
}
// figure out digit value
uint8_t digits[4] = {0};
for (uint8_t digit = 0; digit < LENGTH(digits); digit++) {
for (uint8_t i = 0; i < LENGTH(number_segments); i++) {
if (number_segments[i] == segments[digit]) {
digits[digit] = i;
}
}
}
const uint16_t meas = digits[0] * 1000 + digits[1] * 100 + digits[2] * 10 + digits[3]; // in deci dBA
if (meas < 1400) { // ignore all segments on out of range values
printf("%u.%u dBA\n", meas / 10, meas % 10);
}
}
}
}
// called after LCD frame (e.g. SPI transaction) is received
static uint8_t timeout = 0;
void post_trans_cb(spi_slave_transaction_t *t) {
if (t) {
ESP_ERROR_CHECK( xQueueSend(meas_queue, t, 512) != pdTRUE ); // send measurement to be displayed
spi_slave_queue_trans(SPI2_HOST, t, portMAX_DELAY); // re-add transaction
}
timeout = 0; // reset timeout since we received data
}
void app_main(void)
{
// check DFU force
gpio_config_t io_conf = {}; // to configure GPIO
// GPIO0 can also be pressed while soft reboot
io_conf.pin_bit_mask = (1ULL << 0); // GPIO to configure
io_conf.intr_type = GPIO_INTR_DISABLE; // disable interrupt
io_conf.mode = GPIO_MODE_INPUT; // set as input
io_conf.pull_down_en = false; // disable pull-down mode
io_conf.pull_up_en = true; // enable pull-up mode
ESP_ERROR_CHECK( gpio_config(&io_conf) ); // configure GPIO
// dedicated DFU button
io_conf.pin_bit_mask = (1ULL << DFU_PIN); // GPIO to configure
io_conf.intr_type = GPIO_INTR_DISABLE; // disable interrupt
io_conf.mode = GPIO_MODE_INPUT; // set as input
io_conf.pull_down_en = 1; // enable pull-down mode
io_conf.pull_up_en = 0; // disable pull-up mode
ESP_ERROR_CHECK( gpio_config(&io_conf) ); // configure GPIO
if (gpio_get_level(DFU_PIN)) { // DFU mode asserted
if (!gpio_get_level(0) || gpio_get_level(DFU_PIN)) { // DFU mode asserted
tud_dfu_runtime_reboot_to_dfu_cb(); // reboot to DFU mode
}
// configure LEDs
io_conf.pin_bit_mask = (1ULL << LED_BOARD); // GPIO to configure
io_conf.intr_type = GPIO_INTR_DISABLE; // disable interrupt
@ -78,10 +278,49 @@ void app_main(void)
tinyusb_config_cdcacm_t amc_cfg = { 0 }; // the configuration uses default values
ESP_ERROR_CHECK( tusb_cdc_acm_init(&amc_cfg) ); // configure CDC ACM
ESP_ERROR_CHECK( tusb_dfu_rf_init() ); // configure DFU runtime (ensures we can use it)
esp_tusb_init_console(TINYUSB_CDC_ACM_0); // log to USB
ESP_LOGI(TAG, "USB initialized");
// configure shutdown
io_conf.pin_bit_mask = (1ULL << GPIO_POWER); // GPIO to configure
io_conf.intr_type = GPIO_INTR_DISABLE; // disable interrupt
io_conf.mode = GPIO_MODE_INPUT; // set as input
io_conf.pull_down_en = 0; // don't use pull-down
io_conf.pull_up_en = 0; // there is already an external pull-up
ESP_ERROR_CHECK( gpio_config(&io_conf) ); // configure GPIO
ESP_ERROR_CHECK( esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL) ); // make sure all sources are disabled
ESP_ERROR_CHECK( esp_sleep_enable_ext0_wakeup(GPIO_POWER, 0) ); // use power button to wake up (low when pressed)
// task to decode and show the measurements
meas_queue = xQueueCreate(MEAS_QUEUE_SIZE, sizeof(spi_slave_transaction_t));
ESP_ERROR_CHECK( NULL == meas_queue );
xTaskCreate(meas_task, "meas_task", 2048, NULL, 4, NULL);
// configure SPI for GM1352 LCD data frame reading
const spi_bus_config_t buscfg = { // SPI bus configuration
.mosi_io_num = GPIO_MOSI,
.miso_io_num = -1,
.sclk_io_num = GPIO_SCLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
};
spi_slave_interface_config_t slvcfg={
.mode = 0,
.spics_io_num = GPIO_CS,
.queue_size = 3,
.flags = 0,
.post_setup_cb = NULL,
.post_trans_cb = post_trans_cb,
};
ESP_ERROR_CHECK( spi_slave_initialize(SPI2_HOST, &buscfg, &slvcfg, SPI_DMA_CH_AUTO) );
spi_slave_transaction_t t; // for the SPI transaction
memset(&t, 0, sizeof(t)); // clear transaction
t.length = FRAME_SIZE * 8; // transaction size
WORD_ALIGNED_ATTR uint8_t recvbuf[FRAME_SIZE]; // SPI receive buffer must be word aligned for DMA
t.rx_buffer = recvbuf; // set buffer
spi_slave_queue_trans(SPI2_HOST, &t, portMAX_DELAY);
ESP_LOGI(TAG, "application ready");
esp_tusb_init_console(TINYUSB_CDC_ACM_0); // log to USB
gpio_set_level(LED_BOARD, 0); // switch running LED on to indicate all is ready
while (1) {
vTaskDelay(1000 / portTICK_PERIOD_MS);
@ -91,5 +330,19 @@ void app_main(void)
} else {
gpio_set_level(LED_BOARD, 1);
}
if (timeout > 0) {
printf(".");
fflush(stdout);
fsync(fileno(stdout));
}
timeout++;
if (timeout > 10) {
printf("\nshutting down due to inactivity\n");
fflush(stdout);
fsync(fileno(stdout));
gpio_set_level(LED_BOARD, 1); // ensure run LED is off
esp_deep_sleep_start(); // go to deep sleep
}
}
}