add firmware

This commit is contained in:
digimint 2026-01-22 19:54:53 -06:00
parent f165ccdc95
commit 580bc8716c
Signed by: digimint
GPG key ID: 8DF1C6FD85ABF748
21 changed files with 6148 additions and 0 deletions

8
firmware/.clangd Normal file
View file

@ -0,0 +1,8 @@
CompileFlags:
Remove:
- "-mlongcalls"
- "-fno-shrink-wrap"
- "-fstrict-volatile-bitfields"
- "-fno-tree-switch-conversion"
- "-std=gnu17"
Compiler: xtensa-esp32-elf-g++

6
firmware/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.cache
.vscode
.devcontainer
build
sdkconfig
sdkconfig.ci

6
firmware/CMakeLists.txt Normal file
View file

@ -0,0 +1,6 @@
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(candytuft-firmware)

53
firmware/README.md Normal file
View file

@ -0,0 +1,53 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | Linux |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | -------- | -------- | ----- |
# Hello World Example
Starts a FreeRTOS task to print "Hello World".
(See the README.md file in the upper level 'examples' directory for more information about examples.)
## How to use example
Follow detailed instructions provided specifically for this example.
Select the instructions depending on Espressif chip installed on your development board:
- [ESP32 Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/stable/get-started/index.html)
- [ESP32-S2 Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/get-started/index.html)
## Example folder contents
The project **hello_world** contains one source file in C language [hello_world_main.c](main/hello_world_main.c). The file is located in folder [main](main).
ESP-IDF projects are built using CMake. The project build configuration is contained in `CMakeLists.txt` files that provide set of directives and instructions describing the project's source files and targets (executable, library, or both).
Below is short explanation of remaining files in the project folder.
```
├── CMakeLists.txt
├── pytest_hello_world.py Python script used for automated testing
├── main
│ ├── CMakeLists.txt
│ └── hello_world_main.c
└── README.md This is the file you are currently reading
```
For more information on structure and contents of ESP-IDF projects, please refer to Section [Build System](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/build-system.html) of the ESP-IDF Programming Guide.
## Troubleshooting
* Program upload failure
* Hardware connection is not correct: run `idf.py -p PORT monitor`, and reboot your board to see if there are any output logs.
* The baud rate for downloading is too high: lower your baud rate in the `menuconfig` menu, and try again.
## Technical support and feedback
Please use the following feedback channels:
* For technical queries, go to the [esp32.com](https://esp32.com/) forum
* For a feature request or bug report, create a [GitHub issue](https://github.com/espressif/esp-idf/issues)
We will get back to you as soon as possible.

View file

@ -0,0 +1,6 @@
idf_component_register(SRCS "src/hello_world_main.c"
"src/iic.c"
"src/lcd/ssd1306.c"
"src/ui/inputs.c"
"src/led/ws2811.c"
INCLUDE_DIRS "include")

View file

@ -0,0 +1,68 @@
menu "Candytuft Pin Mapping"
menu "Inputs"
config PIN_WHEEL_A
int "Rotary encoder pin A"
range 0 39
default 21
help
If using a non-standard mainboard, set this value to the GPIO connected to pin A of the rotary encoder.
config PIN_WHEEL_B
int "Rotary encoder pin B"
range 0 39
default 22
help
If using a non-standard mainboard, set this value to the GPIO connected to pin B of the rotary encoder.
config KEY_FORWARD
int "Front Panel Forward Key Mapping"
range 0 39
default 4
help
If using a non-standard mainboard, set this value to the GPIO connected to the Forward key.
config KEY_BACKSPACE
int "Front Panel Backspace Key Mapping"
range 0 39
default 13
help
If using a non-standard mainboard, set this value to the GPIO connected to the Backspace key.
config KEY_DONE
int "Front Panel Done Key Mapping"
range 0 39
default 14
help
If using a non-standard mainboard, set this value to the GPIO connected to the Done key.
config KEY_CANCEL
int "Front Panel Cancel Key Mapping"
range 0 39
default 18
help
If using a non-standard mainboard, set this value to the GPIO connected to the Cancel key.
config KEY_OPTION
int "Front Panel Option Key Mapping"
range 0 39
default 19
help
If using a non-standard mainboard, set this value to the GPIO connected to the Option key.
endmenu
menu "LCD"
config LCD_SCL
int "LCD panel SCL pin mapping"
range 0 39
default 25
help
If using a non-standard mainboard, set this value to the GPIO connected to the LCD panel's SCL pin.
config LCD_SDA
int "LCD panel SDA pin mapping"
range 0 39
default 26
help
If using a non-standard mainboard, set this value to the GPIO connected to the LCD panel's SDA pin.
endmenu
endmenu

View file

View file

@ -0,0 +1,23 @@
/* MX-008 "Onyx Sovereign" -02 "Kingmaker"
* ============================================================================
* === IIC interface configuration
* ============================================================================
*
* Alexis Maya-Isabelle Shuping
* 29th March 2021
* University of Florida
*/
#pragma once
#include <driver/i2c.h>
#define T_IIC_INIT ("IIC INIT")
#define T_IIC_LCD ("IICL")
#define IIC_NUM_LCD (I2C_NUM_0)
void iicInit(i2c_port_t port, int sclNum, int sdaNum, bool pullUps, int clkRate);
void iicCommand(i2c_port_t port, uint_fast8_t targetAddress, uint_fast8_t toSend);
void iicData(i2c_port_t port, uint_fast8_t targetAddress, uint_fast8_t toSend);

View file

@ -0,0 +1,9 @@
#pragma once
#include <stdint.h>
typedef struct font_t {
const char* fTable;
uint16_t width;
uint16_t height;
uint16_t cSize;
} font_t;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,97 @@
/**
* ============================================================================
* === SSD1306 128x64 IIC LCD Interface
* ============================================================================
*
* Provides a simple interface for rendering text to an SSD1306 LCD.
*
* digimint
*/
#pragma once
#include "FontComicSansMS16.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <stdbool.h>
#include <stdint.h>
// LCD target address
#define IIC_LCD_ADDR (0x3C)
#define LCD_TAG "LCD"
extern uint8_t LCD_data[8][128];
extern bool LCD_rowChanged[8];
/**
* Initialize the LCD interface.
*
* This command configures the IIC bus, sends the proper initialization commands
* to the LCD, and spawns a task to update the display regularly.
*
* @param updateTaskHandle If this variable is not NULL, a handle to the display
* update task will be stored in it.
*/
void LCD_init(TaskHandle_t* updateTaskHandle);
/**
* Render a single character to the LCD buffer.
*
* @warning This function does not perform bounds checking.
*
* @param font The font to use for text rendering.
* @param oChr The character to render.
* @param x The x-coordinate for the top-left of the character.
* @param y The y-coordinate for the top-left of the character.
* @param invert Whether the character should be inverted before display.
*/
void LCD_renderChar(font_t font, const char oChr, uint16_t x, uint16_t y, bool invert);
/**
* Render text to the LCD buffer.
*
* @warning This function does not protect against buffer overrun.
*
* @param font The font to use for text rendering.
* @param str The text to render.
* @param x The x-coordinate for the top-left of the first character.
* @param y The y-coordinate for the top-left of the first character.
* @param invert Whether the character should be inverted before display.
*/
void LCD_renderText(font_t font, const char* str, uint16_t x, uint16_t y, bool invert);
/**
* Render text in Comic Sans
*
* A convenience macro that expands to
*
* `LCD_renderText(FontComicSansMS16, str, x, y);`
*/
static inline void LCD_renderTextCS(const char* str, uint16_t x, uint16_t y){
LCD_renderText(FontComicSansMS16, str, x, y, false);
}
/**
* Manually force an immediate update of the LCD.
*
* Note that this function will be called automatically by the LCD update task
* during normal operation, so it generally should not be manually invoked.
*
* A semaphore is used to ensure that only one task attempts to communicate with
* the display at a time.
*
* @param block If true, then the calling task will block until the LCD is
* available for communication. Otherwise, if the LCD is not immediately
* available, the function will simply return `false`.
*
* @return `true` if the LCD was successfully updated; `false` otherwise.
*
* @note This function will only return false if two conditions apply:
* (1) `block` is false; and
* (2) The LCD semaphore has been taken (i.e. the LCD is currently busy)
*/
bool LCD_updateDisplay(bool block);

View file

@ -0,0 +1,28 @@
#pragma once
#include <esp_attr.h>
#include <stdint.h>
#include <stddef.h>
#define LED_PIN (23)
typedef enum LEDStripSpeed{
WS2811_LOW_SPEED = 10000000, // One tick == 0.1us
WS2811_HIGH_SPEED = 20000000 // One tick == 0.05us
} LEDStripSpeed_t;
typedef struct PACKED_ATTR rgbPacket {
uint8_t r;
uint8_t g;
uint8_t b;
} rgbPacket_t;
typedef void (*ledRenderFunction_t)(
rgbPacket_t* buf,
size_t led_count,
unsigned long frame
);
extern uint8_t saturation;
void init_leds();

View file

@ -0,0 +1,34 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/event_groups.h>
#define INPUTS_TAG ("INPUT")
extern TaskHandle_t T_ReadInputs_Handle;
extern EventGroupHandle_t EV_InputEventGroupHandle;
typedef enum InputEventGroupFlags {
INPUT_EVENT_WHEEL = (1 << 0),
INPUT_EVENT_KEY_FORWARD = (1 << 1),
INPUT_EVENT_KEY_BACKSPACE = (1 << 2),
INPUT_EVENT_KEY_DONE = (1 << 3),
INPUT_EVENT_KEY_CANCEL = (1 << 4),
INPUT_EVENT_KEY_OPTION = (1 << 5)
} UIEventGroupFlags_t;
typedef struct InputState {
bool forward, backspace, done, cancel, option;
int wheel;
uint8_t _wheelQuadrant;
} InputState_t;
void init_inputs();
InputState_t waitForInputEvents(UIEventGroupFlags_t events, BaseType_t xWaitForAll, TickType_t xTicksToWait);
InputState_t getInputState();

View file

@ -0,0 +1,163 @@
/*
* SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <stdio.h>
#include <inttypes.h>
#include "driver/i2c_types.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/projdefs.h"
#include "hal/gpio_types.h"
#include "hal/i2c_types.h"
// #include "portmacro.h"
// #include "portmacro.h"
// #include "portmacro.h"
// #include "portmacro.h"
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "lcd/ssd1306.h"
#include "led/ws2811.h"
#include "driver/gpio.h"
#include "driver/i2c_master.h"
#include "soc/clk_tree_defs.h"
#include "ui/inputs.h"
void T_watchInputs(void* params){
while(1){
EventBits_t changed = xEventGroupWaitBits(
EV_InputEventGroupHandle,
(
INPUT_EVENT_KEY_BACKSPACE
| INPUT_EVENT_KEY_FORWARD
| INPUT_EVENT_KEY_CANCEL
| INPUT_EVENT_KEY_DONE
| INPUT_EVENT_KEY_OPTION
| INPUT_EVENT_WHEEL
),
pdTRUE,
pdFALSE,
portMAX_DELAY
);
InputState_t curInputs = getInputState();
if(changed & INPUT_EVENT_KEY_FORWARD){
if(curInputs.forward){
ESP_LOGI("UI", "Forward Pressed");
}else{
ESP_LOGI("UI", "Forward Released");
}
}
if(changed & INPUT_EVENT_KEY_BACKSPACE){
if(curInputs.backspace){
ESP_LOGI("UI", "Backspace Pressed");
}else{
ESP_LOGI("UI", "Backspace Released");
}
}
if(changed & INPUT_EVENT_KEY_DONE){
if(curInputs.done){
ESP_LOGI("UI", "Done Pressed");
}else{
ESP_LOGI("UI", "Done Released");
}
}
if(changed & INPUT_EVENT_KEY_CANCEL){
if(curInputs.cancel){
ESP_LOGI("UI", "Cancel Pressed");
}else{
ESP_LOGI("UI", "Cancel Released");
}
}
if(changed & INPUT_EVENT_KEY_OPTION){
if(curInputs.option){
ESP_LOGI("UI", "Option Pressed");
}else{
ESP_LOGI("UI", "Option Released");
}
}
if(changed & INPUT_EVENT_WHEEL){
ESP_LOGI("UI", "Wheel: %d", curInputs.wheel);
if(curInputs.wheel < 0){
saturation = 0;
}else if(curInputs.wheel > 255){
saturation = 255;
}else{
saturation = (uint8_t)curInputs.wheel;
}
}
}
}
void app_main(void)
{
printf("Hello world! This is Candytuft 40M9K!\n");
LCD_init(NULL);
LCD_renderTextCS("Comic", 0, 0);
LCD_renderTextCS("Sans", 0, 16);
LCD_renderTextCS("LCD", 0, 32);
LCD_updateDisplay(true);
init_inputs();
init_leds();
xTaskCreate(T_watchInputs, "Watch inputs", 2048, NULL, 2, NULL);
/* Print chip information */
esp_chip_info_t chip_info;
uint32_t flash_size;
esp_chip_info(&chip_info);
printf("This is %s chip with %d CPU core(s), %s%s%s%s, ",
CONFIG_IDF_TARGET,
chip_info.cores,
(chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "",
(chip_info.features & CHIP_FEATURE_BT) ? "BT" : "",
(chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "",
(chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : "");
unsigned major_rev = chip_info.revision / 100;
unsigned minor_rev = chip_info.revision % 100;
printf("silicon revision v%d.%d, ", major_rev, minor_rev);
if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
printf("Get flash size failed");
return;
}
printf("%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024),
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");
printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size());
while(1){
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
// for (int i = 10; i >= 0; i--) {
// printf("Restarting in %d seconds...\n", i);
// vTaskDelay(1000 / portTICK_PERIOD_MS);
// }
// printf("Restarting now.\n");
// fflush(stdout);
// esp_restart();
}

57
firmware/main/src/iic.c Normal file
View file

@ -0,0 +1,57 @@
/* MX-008 "Onyx Sovereign" -02 "Kingmaker"
* ============================================================================
* === IIC interface configuration
* ============================================================================
*
* Alexis Maya-Isabelle Shuping
* 29th March 2021
* University of Florida
*/
#include "iic.h"
#include <driver/i2c.h>
#include <esp_log.h>
#include <esp_heap_caps.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
void iicInit(i2c_port_t port, int sclNum, int sdaNum, bool pullUps, int clkRate){
ESP_LOGI(T_IIC_INIT, "Initializing LCD interface %d at %dHz (SCL pin %d; SDA pin %d; pullups %s)", (int)port, clkRate, sclNum, sdaNum, pullUps ? "enabled" : "disabled");
i2c_config_t my2c = (i2c_config_t) {
.mode = I2C_MODE_MASTER,
.sda_io_num = sdaNum,
.scl_io_num = sclNum,
.sda_pullup_en = pullUps ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE,
.scl_pullup_en = pullUps ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE,
.master.clk_speed = clkRate
};
i2c_param_config(port, &my2c);
i2c_driver_install(port, my2c.mode, 0, 0, 0);
}
void iicCommand(i2c_port_t port, uint_fast8_t targetAddress, uint_fast8_t toSend){
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
ESP_ERROR_CHECK(i2c_master_write_byte(cmd, targetAddress << 1 | 0, true));
ESP_ERROR_CHECK(i2c_master_write_byte(cmd, 0x00, true));
ESP_ERROR_CHECK(i2c_master_write_byte(cmd, toSend, true));
i2c_master_stop(cmd);
ESP_ERROR_CHECK(i2c_master_cmd_begin(port, cmd, 1000 / portTICK_PERIOD_MS));
i2c_cmd_link_delete(cmd);
}
void iicData(i2c_port_t port, uint_fast8_t targetAddress, uint_fast8_t toSend){
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
ESP_ERROR_CHECK(i2c_master_write_byte(cmd, targetAddress << 1 | 0, true));
ESP_ERROR_CHECK(i2c_master_write_byte(cmd, 0x40, true));
ESP_ERROR_CHECK(i2c_master_write_byte(cmd, toSend, true));
i2c_master_stop(cmd);
ESP_ERROR_CHECK(i2c_master_cmd_begin(port, cmd, 1000 / portTICK_PERIOD_MS));
i2c_cmd_link_delete(cmd);
}

View file

@ -0,0 +1,189 @@
/**
* ============================================================================
* === SSD1306 128x64 IIC LCD Interface
* ============================================================================
*
* Provides a simple interface for rendering text to an SSD1306 LCD.
*
* digimint
*/
#include "iic.h"
#include "lcd/FontComicSansMS16.h"
#include "lcd/ssd1306.h"
#include "lcd/SymbolsFont.h"
#include "sdkconfig.h"
#include <driver/i2c.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/portmacro.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
uint8_t LCD_data[8][128];
bool LCD_rowChanged[8];
StaticSemaphore_t _lcSemaphore_buf;
volatile bool _lcDirty = false;
SemaphoreHandle_t _lcSemaphore = NULL;
static inline void _setPx(uint16_t x, uint16_t y){
uint16_t bank = y >> 3;
uint16_t posi = y & 0b111;
LCD_data[bank][x] |= (1 << posi);
LCD_rowChanged[bank] = true;
}
static inline void _clrPx(uint16_t x, uint16_t y){
uint16_t bank = y >> 3;
uint16_t posi = y & 0b111;
LCD_data[bank][x] &= ~(1 << posi);
LCD_rowChanged[bank] = true;
}
void LCD_renderChar(font_t font, const char oChr, uint16_t x, uint16_t y, bool invert){
char chr = oChr;
if(chr < ' ' || chr > '~'){
chr = ' ';
}
chr -= ' ';
uint_fast16_t charsPerRow = ((font.width & 0b111) == 0) ? (font.width >> 3) : ((font.width >> 3) + 1);
const char* toRender = &(font.fTable)[chr*(font.cSize)];
for(uint16_t xx = 0; xx < font.width; ++xx){
for(uint16_t yy = 0; yy < font.height; ++yy){
if(
toRender[yy*charsPerRow + (xx >> 3)]
& (1 << (7-(xx & 0b111)))
){
invert ? _clrPx(xx+x, yy+y) : _setPx(xx+x, yy+y);
}else{
invert ? _setPx(xx+x, yy+y) : _clrPx(xx+x, yy+y);
}
}
}
}
void LCD_renderText(font_t font, const char* str, uint16_t x, uint16_t y, bool invert){
uint_fast16_t i = 0;
while(*str != 0){
LCD_renderChar(font, *str, font.width*i+x, y, invert);
++str;
++i;
}
}
bool LCD_updateDisplay(bool block){
if(!xSemaphoreTake(_lcSemaphore, 0)){
if(!block) return false;
else xSemaphoreTake(_lcSemaphore, portMAX_DELAY);
}
for(uint32_t j = 0; j < 8; ++j){
// if(LCD_rowChanged[j] == false) continue;
for(uint32_t i = 0; i < 128; ++i){
iicData(IIC_NUM_LCD, IIC_LCD_ADDR, LCD_data[j][i]);
}
LCD_rowChanged[j] = false;
}
xSemaphoreGive(_lcSemaphore);
return true;
}
// void T_LCD_updateTask(void* params){
// while(1){
// Display_render();
// vTaskDelay(50 / portTICK_PERIOD_MS);
// bool needsUpdate = false;
// for(uint8_t i = 0; i < 8; ++i) needsUpdate = needsUpdate || LCD_rowChanged[i];
// if(needsUpdate == true) LCD_updateDisplay(true);
// }
// }
void LCD_init(TaskHandle_t* updateTaskHandle){
ESP_LOGI(LCD_TAG, "Initializing SSD1306 LCD Interface...");
// Initialize the semaphore for LCD access.
_lcSemaphore = xSemaphoreCreateBinaryStatic(&_lcSemaphore_buf);
xSemaphoreGive(_lcSemaphore);
ESP_LOGD(LCD_TAG, " Initializing IIC");
iicInit(
IIC_NUM_LCD,
CONFIG_LCD_SCL,
CONFIG_LCD_SDA,
true,
200000
);
ESP_LOGD(LCD_TAG, " Sending LCD initialization commands");
/* Shouldn't be necessary to take the semaphore here, but better safe than
* sorry.
*/
if(!xSemaphoreTake(_lcSemaphore, 0)) esp_system_abort("Semaphore error during LCD initialization!");
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xAE);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xD5);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x80);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xA8);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x3F);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xD3);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x00);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x40);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x8D);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x14);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x20);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x00);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xA1);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xC8);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xDA);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x12);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x81);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xCF);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xD9);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xF1);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xDB);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x40);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xA4);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xA6);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0xAF);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x00);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x10);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x21);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x00);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x7F);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x22);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x00);
iicCommand(IIC_NUM_LCD, IIC_LCD_ADDR, 0x07);
xSemaphoreGive(_lcSemaphore);
ESP_LOGD(LCD_TAG, " Initializing data buffer");
for(uint32_t j = 0; j < 8; ++j){
for(uint32_t i = 0; i < 128; ++i){
LCD_data[j][i] = 0;
}
}
ESP_LOGD(LCD_TAG, " Clearing Display");
if(!LCD_updateDisplay(false)){
ESP_LOGW(LCD_TAG, "Unable to clear LCD display (communication semaphore already in use).");
}
// ESP_LOGD(LCD_TAG, " Spawning LCD update task");
// TaskHandle_t tsk;
// if(updateTaskHandle == NULL){
// updateTaskHandle = &tsk;
// }
// xTaskCreate(&T_LCD_updateTask, "LCD Update Task", 2048, NULL, 2, updateTaskHandle);
ESP_LOGI(LCD_TAG, "Ready.");
}

View file

@ -0,0 +1,303 @@
#include <driver/rmt_encoder.h>
#include <driver/rmt_tx.h>
#include <esp_attr.h>
#include <esp_log.h>
#include <math.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "led/ws2811.h"
#include "driver/rmt_common.h"
#include "driver/rmt_types.h"
#include "esp_err.h"
#include "hal/rmt_types.h"
#include "soc/clk_tree_defs.h"
#include "soc/gpio_num.h"
#define SYMBOLS_PER_LED (8)
#define SYMBOLS_PER_PACKET (3*SYMBOLS_PER_LED)
uint8_t saturation = 255;
// The 'duration' here is in ticks - the actual time is controlled by the tick
// rate. In our case, low-speed == 10MHz tick rate; high-speed == 20MHz tick
// rate. Thus, a high-duration of 5 means 0.5us at low-speed and 0.25 at
// high-speed, which matches the datasheet.
const rmt_symbol_word_t ws2811_0 = {
.duration0 = 5,
.level0 = 1,
.duration1 = 20,
.level1 = 0
};
const rmt_symbol_word_t ws2811_1 = {
.duration0 = 12,
.level0 = 1,
.duration1 = 13,
.level1 = 0
};
const rmt_symbol_word_t ws2811_rst = {
.duration0 = 10,
.level0 = 1,
.duration1 = 202, // >= 51uS even at high-speed.
.level1 = 0
};
typedef struct _renderFunctionLL {
ledRenderFunction_t fn;
struct _renderFunctionLL* next;
} _renderFunctionLL_t;
typedef struct _ledTaskParams {
size_t ledCount;
} _ledTaskParams_t;
IRAM_ATTR size_t _encodeOne(
const rgbPacket_t* pkt,
unsigned int offset,
unsigned int write_limit,
rmt_symbol_word_t* to_write,
size_t buf_offset
){
if(offset > SYMBOLS_PER_PACKET){
return 0;
}
size_t symbols_written = 0;
for(int i = offset; i < SYMBOLS_PER_PACKET; ++i){
if(symbols_written == write_limit) break;
if(i < SYMBOLS_PER_LED){
to_write[buf_offset + i] = (pkt -> r) & (1 << (7 - i)) ? ws2811_1 : ws2811_0;
}else if(i < 2*SYMBOLS_PER_LED){
unsigned int ii = i - SYMBOLS_PER_LED;
to_write[buf_offset + i] = (pkt -> g) & (1 << (7 - ii)) ? ws2811_1 : ws2811_0;
}else{
unsigned int ii = i - 2*SYMBOLS_PER_LED;
to_write[buf_offset + i] = (pkt -> b) & (1 << (7 - ii)) ? ws2811_1 : ws2811_0;
}
++symbols_written;
}
return symbols_written;
}
volatile int tmp = 0;
IRAM_ATTR size_t _encode(
const void* data,
size_t data_size,
size_t symbols_written,
size_t symbols_free,
rmt_symbol_word_t* symbol_buf,
bool* done,
void* arg
){
tmp = data_size;
if(symbols_free == 0) return 0; // Can't write anything.
const rgbPacket_t* packets = (rgbPacket_t*)data;
unsigned int packet_index = symbols_written / SYMBOLS_PER_PACKET;
unsigned int packet_offset = symbols_written % SYMBOLS_PER_PACKET;
unsigned int packet_count = data_size / sizeof(rgbPacket_t);
// ESP_LOGI("LED", "sz %lu", (unsigned long)data_size);
if(packet_index >= packet_count){
// Write the reset symbol and finish
symbol_buf[0] = ws2811_rst;
(*done) = true;
return 1;
}else{
if(symbols_free < 24) return 0;
(*done) = false;
return (size_t)_encodeOne(
&packets[packet_index],
/*packet_offset */ 0,
/* symbols_free */ 24,
symbol_buf,
0
);
}
}
rgbPacket_t fromHSV(uint8_t h, uint8_t s, uint8_t v){
double sd = (double)s / 255.0;
double vd = (double)v / 255.0;
double hd = (double)h * (360.0 / 255.0);
double max = vd;
double chroma = sd * vd;
double min = max - chroma;
double hPrime = hd >= 300.0 ? (hd - 360.0) / 60.0 : hd / 60.0;
double r, g, b;
if(hPrime < 1.0){
if(hPrime < 0.0){
r = max;
g = min;
b = min - hPrime * chroma;
}else{
r = max;
g = min + hPrime * chroma;
b = min;
}
}else if(hPrime < 3.0){
if(hPrime < 2.0){
r = min - (hPrime - 2.0) * chroma;
g = max;
b = min;
}else{
r = min;
g = max;
b = min + (hPrime - 2.0) * chroma;
}
}else{
if(hPrime < 4.0){
r = min;
g = min - (hPrime - 4.0) * chroma;
b = max;
}else{
r = min + (hPrime - 4.0) * chroma;
g = min;
b = max;
}
}
return (rgbPacket_t){
.r = (uint8_t)(r * 255.0),
.g = (uint8_t)(g * 255.0),
.b = (uint8_t)(b * 255.0)
};
}
void _testRenderer(rgbPacket_t* buf, size_t led_count, unsigned long frame){
for(size_t i = 0; i < led_count; ++i){
buf[i] = fromHSV(
(uint8_t)(((frame + (unsigned long)i)*2) % 255UL),
saturation,
255
);
// buf[i] = (rgbPacket_t){
// .r = ((ii % 3UL) == 0UL) ? (uint8_t)255 : (uint8_t)0,
// .g = ((ii % 3UL) == 1UL) ? (uint8_t)255 : (uint8_t)0,
// .b = ((ii % 3UL) == 2UL) ? (uint8_t)255 : (uint8_t)0
// };
}
}
rmt_channel_handle_t ledChannel;
rmt_encoder_handle_t ledEncoder;
TaskHandle_t ledTask;
void T_ledTask(void* params_raw){
_ledTaskParams_t params = *((_ledTaskParams_t*)params_raw);
rgbPacket_t* packetBufA = (rgbPacket_t*) malloc(
(params.ledCount)*sizeof(rgbPacket_t)
);
rgbPacket_t* packetBufB = (rgbPacket_t*) malloc(
(params.ledCount)*sizeof(rgbPacket_t)
);
const rmt_transmit_config_t myTxConfig = {
.loop_count = 0,
};
unsigned long frame = 0;
rgbPacket_t* active_buf = packetBufA;
rgbPacket_t* next_buf = packetBufB;
_testRenderer(
next_buf,
params.ledCount,
frame++
);
while(1){
active_buf = next_buf;
next_buf = (active_buf == packetBufA) ? packetBufB : packetBufA;
ESP_ERROR_CHECK(
rmt_transmit(
ledChannel,
ledEncoder,
active_buf,
(params.ledCount)*sizeof(rgbPacket_t),
&myTxConfig
)
);
_testRenderer(
next_buf,
params.ledCount,
frame++
);
ESP_ERROR_CHECK(
rmt_tx_wait_all_done(
ledChannel,
portMAX_DELAY
)
);
vTaskDelay(20 / portTICK_PERIOD_MS);
}
}
void init_leds(){
ESP_LOGI("LED", "Initializing RMT channel...");
rmt_tx_channel_config_t ledChannelCfg = {
.gpio_num = (gpio_num_t)LED_PIN,
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = WS2811_HIGH_SPEED,
.mem_block_symbols = 128,
.trans_queue_depth = 4,
.intr_priority = 2
};
ESP_ERROR_CHECK(
rmt_new_tx_channel(
&ledChannelCfg,
&ledChannel
)
);
ESP_LOGI("LED", "Configuring encoder...");
const rmt_simple_encoder_config_t ledCfg = {
.callback = _encode,
.min_chunk_size = 24,
.arg = NULL
};
ESP_ERROR_CHECK(
rmt_new_simple_encoder(
&ledCfg,
&ledEncoder
)
);
_ledTaskParams_t params = {
.ledCount = 100
};
ESP_LOGI("LED", "Activating RMT channel...");
ESP_ERROR_CHECK(
rmt_enable(ledChannel)
);
ESP_LOGI("LED", "Spawning task...");
xTaskCreate(
T_ledTask,
"LED update task",
2048,
(void*)&params,
2,
&ledTask
);
ESP_LOGI("LED", "Done!");
}

View file

@ -0,0 +1,240 @@
#include <stdbool.h>
#include <driver/gpio.h>
#include <esp_attr.h>
#include <esp_err.h>
#include <esp_log.h>
#include <sdkconfig.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/event_groups.h>
#include <freertos/projdefs.h>
#include <freertos/semphr.h>
#include <time.h>
#include "ui/inputs.h"
TaskHandle_t T_ReadInputs_Handle;
EventGroupHandle_t EV_InputEventGroupHandle;
SemaphoreHandle_t inputSemaphore;
StaticSemaphore_t _inputSemaphoreBuf;
StaticEventGroup_t _inputEventGroupBuf;
InputState_t _curInputState = {
.forward = false,
.backspace = false,
.done = false,
.cancel = false,
.option = false,
.wheel = 0,
._wheelQuadrant = 0b11
}; // NOTE: take inputSemaphore before accessing
typedef enum _InputEventType{
INPUT_EVENT_TYPE_KEY,
INPUT_EVENT_TYPE_WHEEL
} _InputEventType_t;
void IRAM_ATTR ISR_keyEvent(void* params){
BaseType_t higherTaskWoken = pdFALSE;
xTaskNotifyIndexedFromISR(
T_ReadInputs_Handle,
0,
INPUT_EVENT_TYPE_KEY,
eSetValueWithOverwrite,
&higherTaskWoken
);
if(higherTaskWoken){
portYIELD_FROM_ISR(higherTaskWoken);
}
}
void IRAM_ATTR ISR_wheelEvent(void* params){
BaseType_t higherTaskWoken = pdFALSE;
xTaskNotifyIndexedFromISR(
T_ReadInputs_Handle,
0,
INPUT_EVENT_TYPE_WHEEL,
eSetValueWithOverwrite,
&higherTaskWoken
);
if(higherTaskWoken){
portYIELD_FROM_ISR(higherTaskWoken);
}
}
InputState_t _getNewInputs(bool doDebounceDelay){
if(doDebounceDelay){
vTaskDelay(10 / portTICK_PERIOD_MS);
}
xSemaphoreTake(inputSemaphore, portMAX_DELAY);
InputState_t newInputs = {
.forward = !gpio_get_level(CONFIG_KEY_FORWARD),
.backspace = !gpio_get_level(CONFIG_KEY_BACKSPACE),
.done = !gpio_get_level(CONFIG_KEY_DONE),
.cancel = !gpio_get_level(CONFIG_KEY_CANCEL),
.option = !gpio_get_level(CONFIG_KEY_OPTION),
.wheel = _curInputState.wheel,
._wheelQuadrant = (
(gpio_get_level(CONFIG_PIN_WHEEL_B) << 1)
| (gpio_get_level(CONFIG_PIN_WHEEL_A))
)
};
if(newInputs._wheelQuadrant == 0b11 && _curInputState._wheelQuadrant != 0b11){
if(_curInputState._wheelQuadrant == 0b01){
++newInputs.wheel;
}else if(_curInputState._wheelQuadrant == 0b10){
--newInputs.wheel;
}
}
xSemaphoreGive(inputSemaphore);
return newInputs;
}
EventBits_t _calcInputDeltas(InputState_t newState){
EventBits_t deltas = 0;
xSemaphoreTake(inputSemaphore, portMAX_DELAY);
if(newState.forward != _curInputState.forward) deltas |= INPUT_EVENT_KEY_FORWARD;
if(newState.backspace != _curInputState.backspace) deltas |= INPUT_EVENT_KEY_BACKSPACE;
if(newState.done != _curInputState.done) deltas |= INPUT_EVENT_KEY_DONE;
if(newState.cancel != _curInputState.cancel) deltas |= INPUT_EVENT_KEY_CANCEL;
if(newState.option != _curInputState.option) deltas |= INPUT_EVENT_KEY_OPTION;
if(newState.wheel != _curInputState.wheel) deltas |= INPUT_EVENT_WHEEL;
xSemaphoreGive(inputSemaphore);
return deltas;
}
/**
* Task that handles reading inputs, updating the internal state, and generating
* events in the EV_InputEventGroup.
*/
void T_ReadInputs(void* params){
while(1){
_InputEventType_t evt = (_InputEventType_t)ulTaskNotifyTakeIndexed(
0,
pdTRUE,
portMAX_DELAY
);
InputState_t newInputs = _getNewInputs(
evt == INPUT_EVENT_TYPE_KEY
);
EventBits_t deltas = _calcInputDeltas(newInputs);
xSemaphoreTake(inputSemaphore, portMAX_DELAY);
_curInputState = newInputs;
xSemaphoreGive(inputSemaphore);
xEventGroupSetBits(EV_InputEventGroupHandle, deltas);
}
}
/**
* Task to ensure that all event-bits are cleared immediately.
*
* If a task starts listening on a UI event, it should *not* receive any UI
* event sent before it started listening. Thus, UI events should be cleared
* immediately. This task perpetually waits for any UI event and clears it.
*/
void T_EnsureInputEGClear(void* params){
while(1){
xEventGroupWaitBits(
EV_InputEventGroupHandle,
0xf,
pdTRUE,
pdFALSE,
portMAX_DELAY
);
}
}
void init_inputs(){
ESP_LOGI(INPUTS_TAG, "Configuring input GPIOs...");
gpio_config_t _wheelConfig = {
.pin_bit_mask = (
(1ULL << CONFIG_PIN_WHEEL_A)
| (1ULL << CONFIG_PIN_WHEEL_B)
),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_POSEDGE
};
gpio_config_t _keyConfig = {
.pin_bit_mask = (
(1ULL << CONFIG_KEY_FORWARD)
| (1ULL << CONFIG_KEY_BACKSPACE)
| (1ULL << CONFIG_KEY_DONE)
| (1ULL << CONFIG_KEY_CANCEL)
| (1ULL << CONFIG_KEY_OPTION)
),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_ANYEDGE
};
ESP_ERROR_CHECK(gpio_config(&_wheelConfig));
ESP_ERROR_CHECK(gpio_config(&_keyConfig));
ESP_LOGI(INPUTS_TAG, "Creating event queue...");
EV_InputEventGroupHandle = xEventGroupCreateStatic(&_inputEventGroupBuf);
inputSemaphore = xSemaphoreCreateBinaryStatic(&_inputSemaphoreBuf);
xSemaphoreGive(inputSemaphore); // Starting value of 1
ESP_LOGI(INPUTS_TAG, "Configuring GPIO interrupts...");
ESP_ERROR_CHECK(gpio_install_isr_service(0));
ESP_ERROR_CHECK(gpio_isr_handler_add(CONFIG_KEY_FORWARD, ISR_keyEvent, NULL));
ESP_ERROR_CHECK(gpio_isr_handler_add(CONFIG_KEY_BACKSPACE, ISR_keyEvent, NULL));
ESP_ERROR_CHECK(gpio_isr_handler_add(CONFIG_KEY_DONE, ISR_keyEvent, NULL));
ESP_ERROR_CHECK(gpio_isr_handler_add(CONFIG_KEY_CANCEL, ISR_keyEvent, NULL));
ESP_ERROR_CHECK(gpio_isr_handler_add(CONFIG_KEY_OPTION, ISR_keyEvent, NULL));
ESP_ERROR_CHECK(gpio_isr_handler_add(CONFIG_PIN_WHEEL_A, ISR_wheelEvent, NULL));
ESP_ERROR_CHECK(gpio_isr_handler_add(CONFIG_PIN_WHEEL_B, ISR_wheelEvent, NULL));
ESP_LOGI(INPUTS_TAG, "Spawning tasks...");
xTaskCreate(T_EnsureInputEGClear, "Keeps Input EG Clear", 2048, NULL, 2, NULL);
xTaskCreate(T_ReadInputs, "Reads front-panel inputs", 2048, NULL, 4, &T_ReadInputs_Handle);
ESP_LOGI(INPUTS_TAG, "Complete!");
}
InputState_t waitForInputEvents(UIEventGroupFlags_t events, BaseType_t xWaitForAll, TickType_t xTicksToWait){
xEventGroupWaitBits(
EV_InputEventGroupHandle,
events,
pdTRUE,
xWaitForAll,
xTicksToWait
);
return getInputState();
}
InputState_t getInputState(){
xSemaphoreTake(inputSemaphore, portMAX_DELAY);
InputState_t curInputs = _curInputState;
xSemaphoreGive(inputSemaphore);
return curInputs;
}

View file

@ -0,0 +1,3 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) 5.3.1 Project Minimal Configuration
#