Cleanup of initial Pager port
This commit is contained in:
parent
b9d6ef5884
commit
dc5bae5448
@ -12,11 +12,11 @@
|
||||
#define ENCODER_A GPIO_NUM_40
|
||||
#define ENCODER_B GPIO_NUM_41
|
||||
#define ENCODER_ENTER GPIO_NUM_7
|
||||
#define USER_BTN GPIO_NUM_0
|
||||
|
||||
#define KB_ROWS 4
|
||||
#define KB_COLS 11
|
||||
|
||||
// Lowercase Keymap
|
||||
static constexpr char keymap_lc[KB_ROWS][KB_COLS] = {
|
||||
{'\0', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'},
|
||||
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\n', '\0'},
|
||||
@ -24,6 +24,7 @@ static constexpr char keymap_lc[KB_ROWS][KB_COLS] = {
|
||||
{'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'}
|
||||
};
|
||||
|
||||
// Uppercase Keymap
|
||||
static constexpr char keymap_uc[KB_ROWS][KB_COLS] = {
|
||||
{'\0', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'},
|
||||
{'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', '\n', '\0'},
|
||||
@ -31,7 +32,7 @@ static constexpr char keymap_uc[KB_ROWS][KB_COLS] = {
|
||||
{'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'}
|
||||
};
|
||||
|
||||
|
||||
// Symbol Keymap
|
||||
static constexpr char keymap_sy[KB_ROWS][KB_COLS] = {
|
||||
{'\0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'},
|
||||
{'.', '/', '+', '-', '=', ':', '\'', '"', '@', '\t', '\0'},
|
||||
@ -39,54 +40,28 @@ static constexpr char keymap_sy[KB_ROWS][KB_COLS] = {
|
||||
{'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'}
|
||||
};
|
||||
|
||||
static QueueHandle_t rotaryMsg;
|
||||
static QueueHandle_t keyboardMsg;
|
||||
|
||||
/**
|
||||
* The callback simulates press and release events, because the T-Deck
|
||||
* keyboard only publishes press events on I2C.
|
||||
* LVGL currently works without those extra release events, but they
|
||||
* are implemented for correctness and future compatibility.
|
||||
*
|
||||
* @param indev_drv
|
||||
* @param data
|
||||
*/
|
||||
static void keyboard_read_callback(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
TpagerKeyboard* kb = (TpagerKeyboard*)lv_indev_get_user_data(indev);
|
||||
uint8_t read_buffer = 0x00;
|
||||
static bool enter_prev = false;
|
||||
char keypress = 0;
|
||||
|
||||
// Defaults
|
||||
data->key = 0;
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
|
||||
char encstate = 0;
|
||||
char keypress = 0;
|
||||
if (xQueueReceive(keyboardMsg, &keypress, pdMS_TO_TICKS(50)) == pdPASS) {
|
||||
TT_LOG_I(TAG, "KEY %c (%d)", keypress, keypress);
|
||||
data->key = keypress;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
}/* else if (xQueueReceive(rotaryMsg, &encstate, pdMS_TO_TICKS(50)) == pdPASS) {
|
||||
data->key = encstate;
|
||||
if (encstate == LV_KEY_NEXT) data->enc_diff = 1;
|
||||
if (encstate == LV_KEY_PREV) data->enc_diff = -1;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
}*/
|
||||
|
||||
//bool enter = !gpio_get_level(ENCODER_ENTER);
|
||||
/*if (enter != enter_prev) {
|
||||
TT_LOG_I(TAG, "Encoder CHANGE %d", enter);
|
||||
data->key = LV_KEY_ENTER;
|
||||
data->state = enter ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
|
||||
enter_prev = enter;
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
static void encoder_read_callback(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
TpagerKeyboard* kb = (TpagerKeyboard*)lv_indev_get_user_data(indev);
|
||||
uint8_t read_buffer = 0x00;
|
||||
const int enterFilterThreshold = 2;
|
||||
static int enterFilter = 0;
|
||||
const int enter_filter_threshold = 2;
|
||||
static int enter_filter = 0;
|
||||
const int pulses_click = 4;
|
||||
static int pulses_prev = 0;
|
||||
|
||||
// Defaults
|
||||
@ -95,53 +70,22 @@ static void encoder_read_callback(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
|
||||
int pulses = kb->getEncoderPulses();
|
||||
int pulse_diff = (pulses - pulses_prev);
|
||||
if ((pulse_diff > 4) || (pulse_diff < -4)) {
|
||||
data->enc_diff = pulse_diff / 4;
|
||||
if ((pulse_diff > pulses_click) || (pulse_diff < -pulses_click)) {
|
||||
data->enc_diff = pulse_diff / pulses_click;
|
||||
pulses_prev = pulses;
|
||||
}
|
||||
// TT_LOG_I(TAG, "Encoder DIFF %d PULSE %d", data->enc_diff, pulses);
|
||||
|
||||
char encstate = 0;
|
||||
char keypress = 0;
|
||||
/*if (xQueueReceive(rotaryMsg, &encstate, pdMS_TO_TICKS(50)) == pdPASS) {
|
||||
//data->key = encstate;
|
||||
if (encstate == LV_KEY_NEXT) data->enc_diff = 1;
|
||||
if (encstate == LV_KEY_PREV) data->enc_diff = -1;
|
||||
if (encstate == LV_KEY_RIGHT) data->enc_diff = 100;
|
||||
if (encstate == LV_KEY_LEFT) data->enc_diff = -100;
|
||||
//data->state = LV_INDEV_STATE_PRESSED;
|
||||
}*/
|
||||
|
||||
|
||||
bool enter = !gpio_get_level(ENCODER_ENTER);
|
||||
if (enter && (enterFilter < enterFilterThreshold)) {
|
||||
enterFilter++;
|
||||
if (enter && (enter_filter < enter_filter_threshold)) {
|
||||
enter_filter++;
|
||||
}
|
||||
if (!enter && (enterFilter > 0))
|
||||
{
|
||||
enterFilter--;
|
||||
if (!enter && (enter_filter > 0)) {
|
||||
enter_filter--;
|
||||
}
|
||||
|
||||
if (enterFilter == enterFilterThreshold) {
|
||||
if (enter_filter == enter_filter_threshold) {
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
}
|
||||
//TT_LOG_I(TAG, "ENTER F %d", enterFilter);
|
||||
}
|
||||
|
||||
static void button_read_callback(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
if(!gpio_get_level(USER_BTN)) { /*Is there a button press? (E.g. -1 indicated no button was pressed)*/
|
||||
data->state = LV_INDEV_STATE_PRESSED; /*Set the pressed state*/
|
||||
TT_LOG_I(TAG, "USR BTN PRESS");
|
||||
|
||||
} else {
|
||||
data->state = LV_INDEV_STATE_RELEASED; /*Set the released state*/
|
||||
}
|
||||
|
||||
data->btn_id = 0; /*Save the last button*/
|
||||
}
|
||||
|
||||
void TpagerKeyboard::processEncoder() {
|
||||
|
||||
}
|
||||
|
||||
void TpagerKeyboard::processKeyboard() {
|
||||
@ -151,28 +95,22 @@ void TpagerKeyboard::processKeyboard() {
|
||||
static bool cap_toggle_armed = true;
|
||||
|
||||
if (keypad->update()) {
|
||||
for (int i=0; i<keypad->pressed_key_count; i++) {
|
||||
for (int i=0; i < keypad->pressed_key_count; i++) {
|
||||
auto row = keypad->pressed_list[i].row;
|
||||
auto col = keypad->pressed_list[i].col;
|
||||
auto hold = keypad->pressed_list[i].hold_time;
|
||||
|
||||
TT_LOG_I(TAG, "KB PRESS R=%d C=%d", row, col);
|
||||
|
||||
if ((row == 1) && (col == 10)) {
|
||||
sym_pressed = true;
|
||||
TT_LOG_I(TAG, "KB SYM ON");
|
||||
}
|
||||
if ((row == 2) && (col == 7)) {
|
||||
shift_pressed = true;
|
||||
TT_LOG_I(TAG, "KB SHFT ON");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ((sym_pressed && shift_pressed) && cap_toggle_armed) {
|
||||
cap_toggle = !cap_toggle;
|
||||
cap_toggle_armed = false;
|
||||
TT_LOG_I(TAG, "KB CAP TOGGLE");
|
||||
}
|
||||
|
||||
for (int i=0; i<keypad->pressed_key_count; i++) {
|
||||
@ -195,53 +133,27 @@ void TpagerKeyboard::processKeyboard() {
|
||||
auto row = keypad->released_list[i].row;
|
||||
auto col = keypad->released_list[i].col;
|
||||
|
||||
TT_LOG_I(TAG, "KB REL R=%d C=%d", row, col);
|
||||
|
||||
if ((row == 1) && (col == 10)) {
|
||||
sym_pressed = false;
|
||||
TT_LOG_I(TAG, "KB SYM OFF");
|
||||
}
|
||||
if ((row == 2) && (col == 7)) {
|
||||
shift_pressed = false;
|
||||
TT_LOG_I(TAG, "KB SHFT OFF");
|
||||
}
|
||||
}
|
||||
|
||||
if ((!sym_pressed && !shift_pressed) && !cap_toggle_armed) {
|
||||
cap_toggle_armed = true;
|
||||
TT_LOG_I(TAG, "KB CAP TOGGLE REARM");
|
||||
}
|
||||
}
|
||||
/*
|
||||
char encstate = encoder.process();
|
||||
char encbtn = 0;
|
||||
switch (encstate) {
|
||||
case DIR_CW:
|
||||
TT_LOG_I(TAG, "ENC CW");
|
||||
encbtn = sym_pressed ? LV_KEY_RIGHT : LV_KEY_NEXT;
|
||||
break;
|
||||
case DIR_CCW:
|
||||
TT_LOG_I(TAG, "ENC CCW");
|
||||
encbtn = sym_pressed ? LV_KEY_LEFT : LV_KEY_PREV;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (encbtn != 0) xQueueSend(rotaryMsg, (void *)&encbtn, portMAX_DELAY);
|
||||
*/
|
||||
}
|
||||
|
||||
bool TpagerKeyboard::start(lv_display_t* display) {
|
||||
//encoder.begin();
|
||||
initEncoder();
|
||||
keypad->init(KB_ROWS, KB_COLS);
|
||||
gpio_input_enable(ENCODER_ENTER);
|
||||
gpio_input_enable(USER_BTN);
|
||||
|
||||
assert(encoderTimer == nullptr);
|
||||
encoderTimer = std::make_unique<tt::Timer>(tt::Timer::Type::Periodic, [this] {
|
||||
processEncoder();
|
||||
assert(inputTimer == nullptr);
|
||||
inputTimer = std::make_unique<tt::Timer>(tt::Timer::Type::Periodic, [this] {
|
||||
processKeyboard();
|
||||
});
|
||||
|
||||
@ -251,40 +163,27 @@ bool TpagerKeyboard::start(lv_display_t* display) {
|
||||
lv_indev_set_display(kbHandle, display);
|
||||
lv_indev_set_user_data(kbHandle, this);
|
||||
|
||||
btnHandle = lv_indev_create();
|
||||
lv_indev_set_type(btnHandle, LV_INDEV_TYPE_BUTTON);
|
||||
static const lv_point_t btn_points[1] = {
|
||||
{472, 111}
|
||||
//{20, 30}
|
||||
};
|
||||
lv_indev_set_button_points(btnHandle, btn_points);
|
||||
lv_indev_set_read_cb(btnHandle, &button_read_callback);
|
||||
lv_indev_set_display(btnHandle, display);
|
||||
lv_indev_set_user_data(btnHandle, this);
|
||||
|
||||
encHandle = lv_indev_create();
|
||||
lv_indev_set_type(encHandle, LV_INDEV_TYPE_ENCODER);
|
||||
lv_indev_set_read_cb(encHandle, &encoder_read_callback);
|
||||
lv_indev_set_display(encHandle, display);
|
||||
lv_indev_set_user_data(encHandle, this);
|
||||
|
||||
encoderTimer->start(20 / portTICK_PERIOD_MS);
|
||||
inputTimer->start(20 / portTICK_PERIOD_MS);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TpagerKeyboard::stop() {
|
||||
assert(encoderTimer);
|
||||
assert(inputTimer);
|
||||
|
||||
encoderTimer->stop();
|
||||
encoderTimer = nullptr;
|
||||
inputTimer->stop();
|
||||
inputTimer = nullptr;
|
||||
|
||||
lv_indev_delete(kbHandle);
|
||||
kbHandle = nullptr;
|
||||
lv_indev_delete(encHandle);
|
||||
encHandle = nullptr;
|
||||
lv_indev_delete(btnHandle);
|
||||
btnHandle = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -293,21 +192,24 @@ bool TpagerKeyboard::isAttached() const {
|
||||
}
|
||||
|
||||
void TpagerKeyboard::initEncoder(void) {
|
||||
const int low_limit = -127;
|
||||
const int high_limit = 126;
|
||||
|
||||
// Accum. count makes it that over- and underflows are automatically compensated.
|
||||
// Prerequisite: watchpoints at low and high limit
|
||||
pcnt_unit_config_t unit_config = {
|
||||
.low_limit = -127,
|
||||
.high_limit = 126,
|
||||
.low_limit = low_limit,
|
||||
.high_limit = high_limit,
|
||||
.flags = { .accum_count = 1 },
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &encPcntUnit));
|
||||
|
||||
//ESP_LOGI(encoder, "set glitch filter");
|
||||
pcnt_glitch_filter_config_t filter_config = {
|
||||
.max_glitch_ns = 1000,
|
||||
};
|
||||
ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(encPcntUnit, &filter_config));
|
||||
|
||||
//ESP_LOGI(encoder, "install pcnt channels");
|
||||
pcnt_chan_config_t chan_a_config = {
|
||||
.edge_gpio_num = ENCODER_A,
|
||||
.level_gpio_num = ENCODER_B,
|
||||
@ -321,34 +223,17 @@ void TpagerKeyboard::initEncoder(void) {
|
||||
pcnt_channel_handle_t pcnt_chan_b = NULL;
|
||||
ESP_ERROR_CHECK(pcnt_new_channel(encPcntUnit, &chan_b_config, &pcnt_chan_b));
|
||||
|
||||
//ESP_LOGI(encoder, "set edge and level actions for pcnt channels");
|
||||
ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_a, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE));
|
||||
ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_a, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE));
|
||||
ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_b, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE));
|
||||
ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_b, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE));
|
||||
|
||||
//ESP_LOGI(encoder, "add watch points and register callbacks");
|
||||
ESP_ERROR_CHECK(pcnt_unit_add_watch_point(encPcntUnit, low_limit));
|
||||
ESP_ERROR_CHECK(pcnt_unit_add_watch_point(encPcntUnit, high_limit));
|
||||
|
||||
|
||||
int watch_points[] = {-127,126};
|
||||
for (size_t i = 0; i < sizeof(watch_points) / sizeof(watch_points[0]); i++) {
|
||||
ESP_ERROR_CHECK(pcnt_unit_add_watch_point(encPcntUnit, watch_points[i]));
|
||||
}
|
||||
/*pcnt_event_callbacks_t cbs = {
|
||||
.on_reach = pcnt_on_reach,
|
||||
};
|
||||
hEncoder.queue = xQueueCreate(10, sizeof(int));
|
||||
ESP_ERROR_CHECK(pcnt_unit_register_event_callbacks(encPcntUnit, &cbs, hEncoder.queue));
|
||||
*/
|
||||
//ESP_LOGI(encoder, "enable pcnt unit");
|
||||
ESP_ERROR_CHECK(pcnt_unit_enable(encPcntUnit));
|
||||
//ESP_LOGI(encoder, "clear pcnt unit");
|
||||
ESP_ERROR_CHECK(pcnt_unit_clear_count(encPcntUnit));
|
||||
//ESP_LOGI(encoder, "start pcnt unit");
|
||||
ESP_ERROR_CHECK(pcnt_unit_start(encPcntUnit));
|
||||
|
||||
//External callback register.
|
||||
//hEncoder.callback = callback;
|
||||
}
|
||||
|
||||
int TpagerKeyboard::getEncoderPulses() {
|
||||
@ -360,7 +245,6 @@ int TpagerKeyboard::getEncoderPulses() {
|
||||
|
||||
extern std::shared_ptr<Tca8418> tca8418;
|
||||
std::shared_ptr<tt::hal::keyboard::KeyboardDevice> createKeyboard() {
|
||||
rotaryMsg = xQueueCreate(5, sizeof(char));
|
||||
keyboardMsg = xQueueCreate(20, sizeof(char));
|
||||
|
||||
return std::make_shared<TpagerKeyboard>(tca8418);
|
||||
|
||||
@ -14,13 +14,12 @@ class TpagerKeyboard : public tt::hal::keyboard::KeyboardDevice {
|
||||
private:
|
||||
lv_indev_t* _Nullable kbHandle = nullptr;
|
||||
lv_indev_t* _Nullable encHandle = nullptr;
|
||||
lv_indev_t* _Nullable btnHandle = nullptr;
|
||||
//Rotary encoder;
|
||||
pcnt_unit_handle_t encPcntUnit = nullptr;
|
||||
std::shared_ptr<Tca8418> keypad;
|
||||
std::unique_ptr<tt::Timer> encoderTimer;
|
||||
std::unique_ptr<tt::Timer> inputTimer;
|
||||
|
||||
void initEncoder(void);
|
||||
void processKeyboard();
|
||||
|
||||
public:
|
||||
TpagerKeyboard(std::shared_ptr<Tca8418> tca) : keypad(std::move(tca)) {}
|
||||
@ -34,8 +33,6 @@ public:
|
||||
bool isAttached() const override;
|
||||
lv_indev_t* _Nullable getLvglIndev() override { return kbHandle; }
|
||||
|
||||
void processEncoder();
|
||||
void processKeyboard();
|
||||
int getEncoderPulses();
|
||||
};
|
||||
|
||||
|
||||
5
Drivers/BQ27220/CMakeLists.txt
Normal file
5
Drivers/BQ27220/CMakeLists.txt
Normal file
@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility
|
||||
)
|
||||
5
Drivers/BQ27220/README.md
Normal file
5
Drivers/BQ27220/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# BQ24295
|
||||
|
||||
Power management: I2C-controlled 3A single cell USB charger with narrow VDC 4.5-5.5V adjustable voltage at 1.5A synchronous boost operation.
|
||||
|
||||
[Datasheet](https://www.ti.com/lit/ds/symlink/bq24295.pdf)
|
||||
372
Drivers/BQ27220/Source/Bq27220.cpp
Normal file
372
Drivers/BQ27220/Source/Bq27220.cpp
Normal file
@ -0,0 +1,372 @@
|
||||
#include "Bq27220.h"
|
||||
#include <Tactility/Log.h>
|
||||
|
||||
#include "esp_sleep.h"
|
||||
|
||||
#define TAG "bq27220"
|
||||
|
||||
#define ARRAYSIZE(a) (sizeof(a) / sizeof(*(a)))
|
||||
|
||||
uint8_t highByte(const uint16_t word) { return (word >> 8) & 0xFF; }
|
||||
uint8_t lowByte(const uint16_t word) { return word & 0xFF; }
|
||||
void swapEndianess(uint16_t &word) { word = (lowByte(word) << 8) | highByte(word); }
|
||||
|
||||
namespace registers {
|
||||
static const uint16_t SUBCMD_CTRL_STATUS = 0x0000U;
|
||||
static const uint16_t SUBCMD_DEVICE_NUMBER = 0x0001U;
|
||||
static const uint16_t SUBCMD_FW_VERSION = 0x0002U;
|
||||
static const uint16_t SUBCMD_HW_VERSION = 0x0003U;
|
||||
static const uint16_t SUBCMD_BOARD_OFFSET = 0x0009U;
|
||||
static const uint16_t SUBCMD_CC_OFFSET = 0x000AU;
|
||||
static const uint16_t SUBCMD_CC_OFFSET_SAVE = 0x000BU;
|
||||
static const uint16_t SUBCMD_OCV_CMD = 0x000CU;
|
||||
static const uint16_t SUBCMD_BAT_INSERT = 0x000DU;
|
||||
static const uint16_t SUBCMD_BAT_REMOVE = 0x000EU;
|
||||
static const uint16_t SUBCMD_SET_SNOOZE = 0x0013U;
|
||||
static const uint16_t SUBCMD_CLEAR_SNOOZE = 0x0014U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_1 = 0x0015U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_2 = 0x0016U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_3 = 0x0017U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_4 = 0x0018U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_5 = 0x0019U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_6 = 0x001AU;
|
||||
static const uint16_t SUBCMD_CAL_TOGGLE = 0x002DU;
|
||||
static const uint16_t SUBCMD_SEALED = 0x0030U;
|
||||
static const uint16_t SUBCMD_RESET = 0x0041U;
|
||||
static const uint16_t SUBCMD_EXIT_CAL = 0x0080U;
|
||||
static const uint16_t SUBCMD_ENTER_CAL = 0x0081U;
|
||||
static const uint16_t SUBCMD_ENTER_CFG_UPDATE = 0x0090U;
|
||||
static const uint16_t SUBCMD_EXIT_CFG_UPDATE_REINIT = 0x0091U;
|
||||
static const uint16_t SUBCMD_EXIT_CFG_UPDATE = 0x0092U;
|
||||
static const uint16_t SUBCMD_RETURN_TO_ROM = 0x0F00U;
|
||||
|
||||
static const uint8_t CMD_CONTROL = 0x00U;
|
||||
static const uint8_t CMD_AT_RATE = 0x02U;
|
||||
static const uint8_t CMD_AT_RATE_TIME_TO_EMPTY = 0x04U;
|
||||
static const uint8_t CMD_TEMPERATURE = 0x06U;
|
||||
static const uint8_t CMD_VOLTAGE = 0x08U;
|
||||
static const uint8_t CMD_BATTERY_STATUS = 0x0AU;
|
||||
static const uint8_t CMD_CURRENT = 0x0CU;
|
||||
static const uint8_t CMD_REMAINING_CAPACITY = 0x10U;
|
||||
static const uint8_t CMD_FULL_CHARGE_CAPACITY = 0x12U;
|
||||
static const uint8_t CMD_AVG_CURRENT = 0x14U;
|
||||
static const uint8_t CMD_TIME_TO_EMPTY = 0x16U;
|
||||
static const uint8_t CMD_TIME_TO_FULL = 0x18U;
|
||||
static const uint8_t CMD_STANDBY_CURRENT = 0x1AU;
|
||||
static const uint8_t CMD_STANDBY_TIME_TO_EMPTY = 0x1CU;
|
||||
static const uint8_t CMD_MAX_LOAD_CURRENT = 0x1EU;
|
||||
static const uint8_t CMD_MAX_LOAD_TIME_TO_EMPTY = 0x20U;
|
||||
static const uint8_t CMD_RAW_COULOMB_COUNT = 0x22U;
|
||||
static const uint8_t CMD_AVG_POWER = 0x24U;
|
||||
static const uint8_t CMD_INTERNAL_TEMPERATURE = 0x28U;
|
||||
static const uint8_t CMD_CYCLE_COUNT = 0x2AU;
|
||||
static const uint8_t CMD_STATE_OF_CHARGE = 0x2CU;
|
||||
static const uint8_t CMD_STATE_OF_HEALTH = 0x2EU;
|
||||
static const uint8_t CMD_CHARGE_VOLTAGE = 0x30U;
|
||||
static const uint8_t CMD_CHARGE_CURRENT = 0x32U;
|
||||
static const uint8_t CMD_BTP_DISCHARGE_SET = 0x34U;
|
||||
static const uint8_t CMD_BTP_CHARGE_SET = 0x36U;
|
||||
static const uint8_t CMD_OPERATION_STATUS = 0x3AU;
|
||||
static const uint8_t CMD_DESIGN_CAPACITY = 0x3CU;
|
||||
static const uint8_t CMD_SELECT_SUBCLASS = 0x3EU;
|
||||
static const uint8_t CMD_MAC_DATA = 0x40U;
|
||||
static const uint8_t CMD_MAC_DATA_SUM = 0x60U;
|
||||
static const uint8_t CMD_MAC_DATA_LEN = 0x61U;
|
||||
static const uint8_t CMD_ANALOG_COUNT = 0x79U;
|
||||
static const uint8_t CMD_RAW_CURRENT = 0x7AU;
|
||||
static const uint8_t CMD_RAW_VOLTAGE = 0x7CU;
|
||||
static const uint8_t CMD_RAW_INTERNAL_TEMPERATURE = 0x7EU;
|
||||
static const uint8_t MAC_BUFFER_START = 0x40U;
|
||||
static const uint8_t MAC_BUFFER_END = 0x5FU;
|
||||
static const uint8_t MAC_DATA_SUM = 0x60U;
|
||||
static const uint8_t MAC_DATA_LEN = 0x61U;
|
||||
static const uint8_t ROM_START = 0x3EU;
|
||||
|
||||
static const uint16_t ROM_FULL_CHARGE_CAPACITY = 0x929DU;
|
||||
static const uint16_t ROM_DESIGN_CAPACITY = 0x929FU;
|
||||
static const uint16_t ROM_OPERATION_CONFIG_A = 0x9206U;
|
||||
static const uint16_t ROM_OPERATION_CONFIG_B = 0x9208U;
|
||||
|
||||
} // namespace registers
|
||||
|
||||
bool Bq27220::configureCapacity(uint16_t designCapacity, uint16_t fullChargeCapacity) {
|
||||
return performConfigUpdate([this, designCapacity, fullChargeCapacity]() {
|
||||
// Set the design capacity
|
||||
if (!writeConfig16(registers::ROM_DESIGN_CAPACITY, designCapacity)) {
|
||||
TT_LOG_E(TAG, "Failed to set design capacity!");
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
// Set full charge capacity
|
||||
if (!writeConfig16(registers::ROM_FULL_CHARGE_CAPACITY, fullChargeCapacity)) {
|
||||
TT_LOG_E(TAG, "Failed to set full charge capacity!");
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool Bq27220::getVoltage(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_VOLTAGE, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getCurrent(int16_t &value) {
|
||||
uint16_t u16 = 0;
|
||||
if (readRegister16(registers::CMD_CURRENT, u16)) {
|
||||
swapEndianess(u16);
|
||||
value = (int16_t)u16;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getBatteryStatus(Bq27220::BatteryStatus &batt_sta) {
|
||||
if (readRegister16(registers::CMD_BATTERY_STATUS, batt_sta.full)) {
|
||||
swapEndianess(batt_sta.full);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getOperationStatus(OperationStatus &oper_sta) {
|
||||
if (readRegister16(registers::CMD_OPERATION_STATUS, oper_sta.full)) {
|
||||
swapEndianess(oper_sta.full);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getTemperature(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_TEMPERATURE, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getFullChargeCapacity(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_FULL_CHARGE_CAPACITY, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getDesignCapacity(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_DESIGN_CAPACITY, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getRemainingCapacity(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_REMAINING_CAPACITY, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getStateOfCharge(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_STATE_OF_CHARGE, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getStateOfHealth(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_STATE_OF_HEALTH, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getChargeVoltageMax(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_CHARGE_VOLTAGE, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::unsealDevice() {
|
||||
uint8_t cmd1[] = {0x00, 0x14, 0x04};
|
||||
if (!write(cmd1, ARRAYSIZE(cmd1))) {
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
uint8_t cmd2[] = {0x00, 0x72, 0x36};
|
||||
if (!write(cmd2, ARRAYSIZE(cmd2))) {
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bq27220::unsealFullAccess()
|
||||
{
|
||||
uint8_t buffer[3];
|
||||
buffer[0] = 0x00;
|
||||
buffer[1] = lowByte((accessKey >> 24));
|
||||
buffer[2] = lowByte((accessKey >> 16));
|
||||
if (!write(buffer, ARRAYSIZE(buffer))) {
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
buffer[1] = lowByte((accessKey >> 8));
|
||||
buffer[2] = lowByte((accessKey));
|
||||
if (!write(buffer, ARRAYSIZE(buffer))) {
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bq27220::exitSealMode() {
|
||||
return sendSubCommand(registers::SUBCMD_SEALED);
|
||||
}
|
||||
|
||||
bool Bq27220::sendSubCommand(uint16_t subCmd, bool waitConfirm)
|
||||
{
|
||||
uint8_t buffer[3];
|
||||
buffer[0] = 0x00;
|
||||
buffer[1] = lowByte(subCmd);
|
||||
buffer[2] = highByte(subCmd);
|
||||
if (!write(buffer, ARRAYSIZE(buffer))) {
|
||||
return false;
|
||||
}
|
||||
if (!waitConfirm) {
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
return true;
|
||||
}
|
||||
constexpr uint8_t statusReg = 0x00;
|
||||
int waitCount = 20;
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
while (waitCount--) {
|
||||
writeRead(&statusReg, 1, buffer, 2);
|
||||
uint16_t *value = reinterpret_cast<uint16_t *>(buffer);
|
||||
if (*value == 0xFFA5) {
|
||||
return true;
|
||||
}
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
}
|
||||
TT_LOG_E(TAG, "Subcommand x%X failed!", subCmd);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::writeConfig16(uint16_t address, uint16_t value) {
|
||||
constexpr uint8_t fixedDataLength = 0x06;
|
||||
const uint8_t msbAccessValue = highByte(address);
|
||||
const uint8_t lsbAccessValue = lowByte(address);
|
||||
|
||||
// Write to access the MSB of Capacity
|
||||
writeRegister8(registers::ROM_START, msbAccessValue);
|
||||
|
||||
// Write to access the LSB of Capacity
|
||||
writeRegister8(registers::ROM_START + 1, lsbAccessValue);
|
||||
|
||||
// Write two Capacity bytes starting from 0x40
|
||||
uint8_t valueMsb = highByte(value);
|
||||
uint8_t valueLsb = lowByte(value);
|
||||
uint8_t configRaw[] = {valueMsb, valueLsb};
|
||||
writeRegister(registers::MAC_BUFFER_START, configRaw, 2);
|
||||
// Calculate new checksum
|
||||
uint8_t checksum = 0xFF - ((msbAccessValue + lsbAccessValue + valueMsb + valueLsb) & 0xFF);
|
||||
|
||||
// Write new checksum (0x60)
|
||||
writeRegister8(registers::MAC_DATA_SUM, checksum);
|
||||
|
||||
// Write the block length
|
||||
writeRegister8(registers::MAC_DATA_LEN, fixedDataLength);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bq27220::configPreamble(bool &isSealed) {
|
||||
int timeout = 0;
|
||||
OperationStatus status;
|
||||
|
||||
// Check access settings
|
||||
if(!getOperationStatus(status)) {
|
||||
TT_LOG_E(TAG, "Cannot read initial operation status!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status.reg.SEC == OperationStatusSecSealed) {
|
||||
isSealed = true;
|
||||
if (!unsealDevice()) {
|
||||
TT_LOG_E(TAG, "Unsealing device failure!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (status.reg.SEC != OperationStatusSecFull) {
|
||||
if (!unsealFullAccess()) {
|
||||
TT_LOG_E(TAG, "Unsealing full access failure!");
|
||||
return false;
|
||||
} else {
|
||||
TT_LOG_I(TAG, "Full access theoretically.");
|
||||
}
|
||||
}
|
||||
|
||||
// Send ENTER_CFG_UPDATE command (0x0090)
|
||||
if (!sendSubCommand(registers::SUBCMD_ENTER_CFG_UPDATE)) {
|
||||
TT_LOG_E(TAG, "Config Update Subcommand failure!");
|
||||
}
|
||||
|
||||
// Confirm CFUPDATE mode by polling the OperationStatus() register until Bit 2 is set.
|
||||
bool isConfigUpdate = false;
|
||||
for (timeout = 30; timeout; --timeout) {
|
||||
getOperationStatus(status);
|
||||
if (status.reg.CFGUPDATE) {
|
||||
isConfigUpdate = true;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
}
|
||||
if (!isConfigUpdate) {
|
||||
TT_LOG_E(TAG, "Update Mode timeout, maybe the access key for full permissions is invalid!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bq27220::configEpilouge(const bool isSealed) {
|
||||
int timeout = 0;
|
||||
OperationStatus status;
|
||||
|
||||
// Exit CFUPDATE mode by sending the EXIT_CFG_UPDATE_REINIT (0x0091) or EXIT_CFG_UPDATE (0x0092) command
|
||||
sendSubCommand(registers::SUBCMD_EXIT_CFG_UPDATE_REINIT);
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
// Confirm that CFUPDATE mode has been exited by polling the OperationStatus() register until bit 2 is cleared
|
||||
for (timeout = 60; timeout; --timeout) {
|
||||
getOperationStatus(status);
|
||||
if (!status.reg.CFGUPDATE) {
|
||||
break;
|
||||
}
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
}
|
||||
if (timeout == 0) {
|
||||
TT_LOG_E(TAG, "Timed out waiting to exit update mode.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the device was previously in SEALED state, return to SEALED mode by sending the Control(0x0030) subcommand
|
||||
if (isSealed) {
|
||||
TT_LOG_D(TAG, "Restore Safe Mode!");
|
||||
exitSealMode();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
103
Drivers/BQ27220/Source/Bq27220.h
Normal file
103
Drivers/BQ27220/Source/Bq27220.h
Normal file
@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/hal/i2c/I2cDevice.h>
|
||||
|
||||
#define BQ27220_ADDRESS 0x55
|
||||
|
||||
class Bq27220 final : public tt::hal::i2c::I2cDevice {
|
||||
|
||||
private:
|
||||
uint32_t accessKey;
|
||||
|
||||
bool unsealDevice();
|
||||
bool unsealFullAccess();
|
||||
bool exitSealMode();
|
||||
bool sendSubCommand(uint16_t subCmd, bool waitConfirm = false);
|
||||
bool writeConfig16(uint16_t address, uint16_t value);
|
||||
bool configPreamble(bool &isSealed);
|
||||
bool configEpilouge(const bool isSealed);
|
||||
|
||||
template<typename T>
|
||||
bool performConfigUpdate(T configUpdateFunc)
|
||||
{
|
||||
bool isSealed = false;
|
||||
|
||||
if (!configPreamble(isSealed)) {
|
||||
return false;
|
||||
}
|
||||
bool result = configUpdateFunc();
|
||||
configEpilouge(isSealed);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public:
|
||||
union BatteryStatus {
|
||||
struct
|
||||
{
|
||||
// Low byte, Low bit first
|
||||
uint16_t DSG : 1; /**< The device is in DISCHARGE */
|
||||
uint16_t SYSDWN : 1; /**< System down bit indicating the system should shut down */
|
||||
uint16_t TDA : 1; /**< Terminate Discharge Alarm */
|
||||
uint16_t BATTPRES : 1; /**< Battery Present detected */
|
||||
uint16_t AUTH_GD : 1; /**< Detect inserted battery */
|
||||
uint16_t OCVGD : 1; /**< Good OCV measurement taken */
|
||||
uint16_t TCA : 1; /**< Terminate Charge Alarm */
|
||||
uint16_t RSVD : 1; /**< Reserved */
|
||||
// High byte, Low bit first
|
||||
uint16_t CHGING : 1; /**< Charge inhibit */
|
||||
uint16_t FC : 1; /**< Full-charged is detected */
|
||||
uint16_t OTD : 1; /**< Overtemperature in discharge condition is detected */
|
||||
uint16_t OTC : 1; /**< Overtemperature in charge condition is detected */
|
||||
uint16_t SLEEP : 1; /**< Device is operating in SLEEP mode when set */
|
||||
uint16_t OCVFALL : 1; /**< Status bit indicating that the OCV reading failed due to current */
|
||||
uint16_t OCVCOMP : 1; /**< An OCV measurement update is complete */
|
||||
uint16_t FD : 1; /**< Full-discharge is detected */
|
||||
} reg;
|
||||
uint16_t full;
|
||||
};
|
||||
|
||||
enum OperationStatusSec {
|
||||
OperationStatusSecSealed = 0b11,
|
||||
OperationStatusSecUnsealed = 0b10,
|
||||
OperationStatusSecFull = 0b01,
|
||||
};
|
||||
|
||||
union OperationStatus {
|
||||
struct
|
||||
{
|
||||
// Low byte, Low bit first
|
||||
bool CALMD : 1; /**< Calibration mode enabled */
|
||||
uint8_t SEC : 2; /**< Current security access */
|
||||
bool EDV2 : 1; /**< EDV2 threshold exceeded */
|
||||
bool VDQ : 1; /**< Indicates if Current discharge cycle is NOT qualified or qualified for an FCC updated */
|
||||
bool INITCOMP : 1; /**< gauge initialization is complete */
|
||||
bool SMTH : 1; /**< RemainingCapacity is scaled by smooth engine */
|
||||
bool BTPINT : 1; /**< BTP threshold has been crossed */
|
||||
// High byte, Low bit first
|
||||
uint8_t RSVD1 : 2; /**< Reserved */
|
||||
bool CFGUPDATE : 1; /**< Gauge is in CONFIG UPDATE mode */
|
||||
uint8_t RSVD0 : 5; /**< Reserved */
|
||||
} reg;
|
||||
uint16_t full;
|
||||
};
|
||||
|
||||
std::string getName() const final { return "BQ27220"; }
|
||||
|
||||
std::string getDescription() const final { return "I2C-controlled CEDV battery fuel gauge"; }
|
||||
|
||||
explicit Bq27220(i2c_port_t port) : I2cDevice(port, BQ27220_ADDRESS), accessKey(0xFFFFFFFF) {}
|
||||
|
||||
bool configureCapacity(uint16_t designCapacity, uint16_t fullChargeCapacity);
|
||||
bool getVoltage(uint16_t &value);
|
||||
bool getCurrent(int16_t &value);
|
||||
bool getBatteryStatus(BatteryStatus &batt_sta);
|
||||
bool getOperationStatus(OperationStatus &oper_sta);
|
||||
bool getTemperature(uint16_t &value);
|
||||
bool getFullChargeCapacity(uint16_t &value);
|
||||
bool getDesignCapacity(uint16_t &value);
|
||||
bool getRemainingCapacity(uint16_t &value);
|
||||
bool getStateOfCharge(uint16_t &value);
|
||||
bool getStateOfHealth(uint16_t &value);
|
||||
bool getChargeVoltageMax(uint16_t &value);
|
||||
};
|
||||
@ -93,8 +93,6 @@ void Tca8418::init(uint8_t numrows, uint8_t numcols) {
|
||||
|
||||
clear_released_list();
|
||||
clear_pressed_list();
|
||||
|
||||
//delayMicroseconds(100);
|
||||
}
|
||||
|
||||
bool Tca8418::update() {
|
||||
@ -105,10 +103,10 @@ bool Tca8418::update() {
|
||||
// TODO: read gpio R7/R6 status? 0x14 bits 7&6
|
||||
// read(0x14, &new_keycode)
|
||||
|
||||
this_update_micros = 0; //micros();
|
||||
// TODO: use tick function to get an update delta time
|
||||
this_update_micros = 0;
|
||||
delta_micros = this_update_micros - last_update_micros;
|
||||
|
||||
// if there is a new event
|
||||
if (key_event > 0) {
|
||||
key_code = key_event & 0x7F;
|
||||
key_down = (key_event & 0x80) >> 7;
|
||||
@ -131,7 +129,7 @@ bool Tca8418::update() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// increment hold times for pressed keys
|
||||
// Increment hold times for pressed keys
|
||||
for (int i=0; i<pressed_key_count; i++) {
|
||||
pressed_list[i].hold_time += delta_micros;
|
||||
}
|
||||
@ -159,7 +157,6 @@ void Tca8418::add_released_key(uint8_t row, uint8_t col) {
|
||||
released_list[0].col = col;
|
||||
}
|
||||
|
||||
|
||||
void Tca8418::remove_pressed_key(uint8_t row, uint8_t col) {
|
||||
if (pressed_key_count == 0)
|
||||
return;
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <Tactility/hal/i2c/I2cDevice.h>
|
||||
|
||||
#define TCA8418_ADDRESS 0x34U
|
||||
@ -38,35 +40,21 @@ public:
|
||||
|
||||
std::string getDescription() const final { return "I2C-controlled keyboard scan IC"; }
|
||||
|
||||
|
||||
explicit Tca8418(i2c_port_t port) : I2cDevice(port, TCA8418_ADDRESS) {
|
||||
delta_micros = 0;
|
||||
last_update_micros = 0;
|
||||
this_update_micros = 0;
|
||||
|
||||
pressed_list = new PressedKey[KEY_EVENT_LIST_SIZE];
|
||||
released_list = new ReleasedKey[KEY_EVENT_LIST_SIZE];
|
||||
matrix_state = new uint16_t[num_rows];
|
||||
matrix_state_prev = new uint16_t[num_rows];
|
||||
}
|
||||
|
||||
~Tca8418() {
|
||||
delete [] matrix_state_prev;
|
||||
delete [] matrix_state;
|
||||
delete [] pressed_list;
|
||||
delete [] released_list;
|
||||
}
|
||||
~Tca8418() {}
|
||||
|
||||
uint8_t num_rows;
|
||||
uint8_t num_cols;
|
||||
|
||||
uint16_t *matrix_state;
|
||||
uint16_t *matrix_state_prev;
|
||||
|
||||
uint32_t delta_micros;
|
||||
|
||||
PressedKey *pressed_list;
|
||||
ReleasedKey *released_list;
|
||||
std::array<PressedKey, KEY_EVENT_LIST_SIZE> pressed_list;
|
||||
std::array<ReleasedKey, KEY_EVENT_LIST_SIZE> released_list;
|
||||
uint8_t pressed_key_count;
|
||||
uint8_t released_key_count;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user