349 lines
11 KiB
C
349 lines
11 KiB
C
/* SPDX-License-Identifier: GPL-3.0-or-later
|
|
* Copyright 2022-2023 King Kévin <kingkevin@cuvoodoo.info>
|
|
*/
|
|
#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"
|
|
#include "tusb_console.h"
|
|
#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
|
|
*/
|
|
static const char* usb_str_desc[USB_STRING_DESCRIPTOR_ARRAY_SIZE] = {
|
|
(char[]){0x09, 0x04}, // 0: language: English
|
|
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
|
|
"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(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
|
|
io_conf.mode = GPIO_MODE_INPUT_OUTPUT; // set as output (push-pull), and input to read state
|
|
io_conf.pull_down_en = 0; // disable pull-down mode
|
|
io_conf.pull_up_en = 0; // disable pull-up mode
|
|
ESP_ERROR_CHECK( gpio_config(&io_conf) ); // configure GPIO
|
|
gpio_set_level(LED_BOARD, 0); // switch running LED off until all is ready
|
|
|
|
// setup USB CDC ACM for printing
|
|
ESP_LOGI(TAG, "USB initialization");
|
|
// get MAC
|
|
uint8_t mac[6];
|
|
esp_read_mac(mac, ESP_MAC_ETH);
|
|
static char usb_serial[13] = {0};
|
|
snprintf(usb_serial, sizeof(usb_serial), "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
|
usb_str_desc[3] = usb_serial;
|
|
const tinyusb_config_t tusb_cfg = {
|
|
.device_descriptor = NULL, // use default USB descriptor
|
|
.string_descriptor = usb_str_desc, // use custom string description to set serial
|
|
.external_phy = false, // use integrated phy
|
|
.configuration_descriptor = NULL,
|
|
};
|
|
ESP_ERROR_CHECK( tinyusb_driver_install(&tusb_cfg) ); // configure USB
|
|
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_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);
|
|
// toggle LED as heart beat
|
|
if (gpio_get_level(LED_BOARD)) {
|
|
gpio_set_level(LED_BOARD, 0);
|
|
} 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
|
|
}
|
|
}
|
|
}
|