forked from espressif/esp-idf
feat(esp_hw_support): Add support for Key Manager
This commit is contained in:
committed by
Mahavir Jain
parent
9746de97ce
commit
3eabb62850
@@ -117,6 +117,10 @@ if(NOT BOOTLOADER_BUILD)
|
|||||||
list(APPEND srcs "esp_ds.c")
|
list(APPEND srcs "esp_ds.c")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(CONFIG_SOC_KEY_MANAGER_SUPPORTED)
|
||||||
|
list(APPEND srcs "esp_key_mgr.c")
|
||||||
|
endif()
|
||||||
|
|
||||||
if(CONFIG_SOC_PAU_SUPPORTED)
|
if(CONFIG_SOC_PAU_SUPPORTED)
|
||||||
list(APPEND srcs "port/pau_regdma.c"
|
list(APPEND srcs "port/pau_regdma.c"
|
||||||
"port/regdma_link.c")
|
"port/regdma_link.c")
|
||||||
|
219
components/esp_hw_support/esp_key_mgr.c
Normal file
219
components/esp_hw_support/esp_key_mgr.c
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
// The Hardware Support layer for Key manager
|
||||||
|
#include "hal/key_mgr_types.h"
|
||||||
|
#include "hal/key_mgr_hal.h"
|
||||||
|
#include "hal/huk_types.h"
|
||||||
|
#include "hal/huk_hal.h"
|
||||||
|
#include "esp_key_mgr.h"
|
||||||
|
#include "hal/clk_gate_ll.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "assert.h"
|
||||||
|
#include "string.h"
|
||||||
|
|
||||||
|
static const char *TAG = "esp_key_mgr";
|
||||||
|
static void key_mgr_wait_for_state(esp_key_mgr_state_t state)
|
||||||
|
{
|
||||||
|
while (key_mgr_hal_get_state() != state) {
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_key_mgr_deploy_key_in_aes_mode(esp_key_mgr_aes_key_config_t *key_config, esp_key_mgr_key_recovery_info_t *key_info)
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Key Deployment");
|
||||||
|
// Reset the Key Manager Clock
|
||||||
|
periph_ll_enable_clk_clear_rst(PERIPH_KEY_MANAGER_MODULE);
|
||||||
|
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_IDLE);
|
||||||
|
uint8_t *huk_recovery_info = (uint8_t *) calloc(1, sizeof(KEY_MGR_HUK_INFO_SIZE));
|
||||||
|
if (!huk_recovery_info) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
uint8_t *key_recovery_info = (uint8_t *) calloc(1, sizeof(KEY_MGR_KEY_RECOVERY_INFO_SIZE));
|
||||||
|
if (!key_recovery_info) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_config->huk_info && key_config->huk_info_size) {
|
||||||
|
if (key_config->huk_info_size != KEY_MGR_HUK_INFO_SIZE) {
|
||||||
|
ESP_LOGE(TAG, "Invalid HUK info given");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Recovering key from given HUK recovery info");
|
||||||
|
// If HUK info is provided then recover the HUK from given info
|
||||||
|
huk_hal_configure(ESP_HUK_MODE_RECOVERY, key_config->huk_info);
|
||||||
|
} else {
|
||||||
|
// Generate new HUK and corresponding HUK info
|
||||||
|
ESP_LOGI(TAG, "Generating new HUK");
|
||||||
|
huk_hal_configure(ESP_HUK_MODE_GENERATION, huk_recovery_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "HUK generated successfully");
|
||||||
|
// Configure deployment mode to AES
|
||||||
|
key_mgr_hal_set_key_generator_mode(ESP_KEY_MGR_KEYGEN_MODE_AES);
|
||||||
|
|
||||||
|
// Set key purpose (XTS/ECDSA)
|
||||||
|
key_mgr_hal_set_key_purpose(key_config->key_purpose);
|
||||||
|
|
||||||
|
if (key_config->use_sw_init_key) {
|
||||||
|
key_mgr_hal_use_sw_init_key();
|
||||||
|
}
|
||||||
|
|
||||||
|
key_mgr_hal_start();
|
||||||
|
key_mgr_hal_continue();
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_LOAD);
|
||||||
|
if (key_config->use_sw_init_key) {
|
||||||
|
assert(key_config->sw_init_key_size == KEY_MGR_SW_INIT_KEY_SIZE);
|
||||||
|
key_mgr_hal_write_sw_init_key(key_config->sw_init_key, key_config->sw_init_key_size);
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Writing Information into Key Manager Registers");
|
||||||
|
key_mgr_hal_write_assist_info(key_config->k2_info, KEY_MGR_K2_INFO_SIZE);
|
||||||
|
key_mgr_hal_write_public_info(key_config->k1_encrypted, KEY_MGR_K1_ENCRYPTED_SIZE);
|
||||||
|
key_mgr_hal_continue();
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_GAIN);
|
||||||
|
key_mgr_hal_read_public_info(key_recovery_info, KEY_MGR_KEY_RECOVERY_INFO_SIZE);
|
||||||
|
ESP_LOG_BUFFER_HEX_LEVEL("HUK INFO", huk_recovery_info, KEY_MGR_HUK_INFO_SIZE, ESP_LOG_INFO);
|
||||||
|
ESP_LOG_BUFFER_HEX_LEVEL("KEY_MGR KEY INFO", key_recovery_info, KEY_MGR_KEY_RECOVERY_INFO_SIZE, ESP_LOG_INFO);
|
||||||
|
|
||||||
|
//TODO - check if HUK is valid just after it is generated
|
||||||
|
if (!key_mgr_hal_is_huk_valid()) {
|
||||||
|
ESP_LOGE(TAG, "HUK is invalid");
|
||||||
|
// TODO - define error code
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "HUK deplpoyed is Valid");
|
||||||
|
|
||||||
|
if (!key_mgr_hal_is_key_deployment_valid(key_config->key_type)) {
|
||||||
|
ESP_LOGE(TAG, "Key deployment is not valid");
|
||||||
|
// Todo - Define respective error code;
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Key deployment valid");
|
||||||
|
// Wait till Key Manager deployment is complete
|
||||||
|
key_mgr_hal_continue();
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_IDLE);
|
||||||
|
//memcpy(&key_info->huk_recovery_info[0], huk_recovery_info, KEY_MGR_HUK_INFO_SIZE);
|
||||||
|
//memcpy(&key_info->key_recovery_info[0], key_recovery_info, KEY_MGR_KEY_RECOVERY_INFO_SIZE);
|
||||||
|
key_info->key_purpose = key_config->key_purpose;
|
||||||
|
free(key_recovery_info);
|
||||||
|
free(huk_recovery_info);
|
||||||
|
key_mgr_hal_set_key_usage(ESP_KEY_MGR_XTS_KEY, ESP_KEY_MGR_USE_OWN_KEY);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_key_mgr_recover_key(esp_key_mgr_key_recovery_info_t *key_recovery_info)
|
||||||
|
{
|
||||||
|
periph_ll_enable_clk_clear_rst(PERIPH_KEY_MANAGER_MODULE);
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_IDLE);
|
||||||
|
huk_hal_configure(ESP_HUK_MODE_RECOVERY, key_recovery_info->huk_recovery_info);
|
||||||
|
if (key_mgr_hal_is_huk_valid()) {
|
||||||
|
ESP_LOGD(TAG, "HUK is invalid");
|
||||||
|
// TODO - define error code
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "purpose = %d", key_recovery_info->key_purpose);
|
||||||
|
key_mgr_hal_set_key_purpose(key_recovery_info->key_purpose);
|
||||||
|
key_mgr_hal_start();
|
||||||
|
key_mgr_hal_continue();
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_LOAD);
|
||||||
|
key_mgr_hal_write_assist_info(key_recovery_info->huk_recovery_info, KEY_MGR_HUK_INFO_SIZE);
|
||||||
|
key_mgr_hal_continue();
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_GAIN);
|
||||||
|
if (!key_mgr_hal_is_key_deployment_valid(key_recovery_info->key_type)) {
|
||||||
|
ESP_LOGD(TAG, "Key deployment is not valid");
|
||||||
|
// Todo - Define respective error code;
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Key deployment valid");
|
||||||
|
key_mgr_hal_continue();
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_IDLE);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_key_mgr_deploy_key_in_ecdh0_mode(esp_key_mgr_ecdh0_key_config_t *key_config, esp_key_mgr_key_recovery_info_t *key_info)
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Key Deployment");
|
||||||
|
// Reset the Key Manager Clock
|
||||||
|
periph_ll_enable_clk_clear_rst(PERIPH_KEY_MANAGER_MODULE);
|
||||||
|
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_IDLE);
|
||||||
|
uint8_t *huk_recovery_info = (uint8_t *) calloc(1, sizeof(KEY_MGR_HUK_INFO_SIZE));
|
||||||
|
if (!huk_recovery_info) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
uint8_t *key_recovery_info = (uint8_t *) calloc(1, sizeof(KEY_MGR_KEY_RECOVERY_INFO_SIZE));
|
||||||
|
if (!key_recovery_info) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_config->huk_info && key_config->huk_info_size) {
|
||||||
|
if (key_config->huk_info_size != KEY_MGR_HUK_INFO_SIZE) {
|
||||||
|
ESP_LOGE(TAG, "Invalid HUK info given");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Recovering key from given HUK recovery info");
|
||||||
|
// If HUK info is provided then recover the HUK from given info
|
||||||
|
huk_hal_configure(ESP_HUK_MODE_RECOVERY, key_config->huk_info);
|
||||||
|
} else {
|
||||||
|
// Generate new HUK and corresponding HUK info
|
||||||
|
ESP_LOGI(TAG, "Generating new HUK");
|
||||||
|
huk_hal_configure(ESP_HUK_MODE_GENERATION, huk_recovery_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "HUK generated successfully");
|
||||||
|
// Configure deployment mode to AES
|
||||||
|
key_mgr_hal_set_key_generator_mode(ESP_KEY_MGR_KEYGEN_MODE_AES);
|
||||||
|
|
||||||
|
// Set key purpose (XTS/ECDSA)
|
||||||
|
key_mgr_hal_set_key_purpose(key_config->key_purpose);
|
||||||
|
|
||||||
|
if (key_config->use_sw_init_key) {
|
||||||
|
key_mgr_hal_use_sw_init_key();
|
||||||
|
}
|
||||||
|
|
||||||
|
key_mgr_hal_start();
|
||||||
|
key_mgr_hal_continue();
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_LOAD);
|
||||||
|
if (key_config->use_sw_init_key) {
|
||||||
|
assert(key_config->sw_init_key_size == KEY_MGR_SW_INIT_KEY_SIZE);
|
||||||
|
key_mgr_hal_write_sw_init_key(key_config->sw_init_key, key_config->sw_init_key_size);
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Writing Information into Key Manager Registers");
|
||||||
|
key_mgr_hal_write_public_info(key_config->k1_G, KEY_MGR_ECDH0_INFO_SIZE);
|
||||||
|
key_mgr_hal_continue();
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_GAIN);
|
||||||
|
key_mgr_hal_read_public_info(key_recovery_info, KEY_MGR_KEY_RECOVERY_INFO_SIZE);
|
||||||
|
key_mgr_hal_read_assist_info(key_config->k2_G);
|
||||||
|
ESP_LOG_BUFFER_HEX_LEVEL("HUK INFO", huk_recovery_info, KEY_MGR_HUK_INFO_SIZE, ESP_LOG_INFO);
|
||||||
|
ESP_LOG_BUFFER_HEX_LEVEL("KEY_MGR KEY INFO", key_recovery_info, KEY_MGR_KEY_RECOVERY_INFO_SIZE, ESP_LOG_INFO);
|
||||||
|
|
||||||
|
//TODO - check if HUK is valid just after it is generated
|
||||||
|
if (!key_mgr_hal_is_huk_valid()) {
|
||||||
|
ESP_LOGE(TAG, "HUK is invalid");
|
||||||
|
// TODO - define error code
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "HUK deplpoyed is Valid");
|
||||||
|
|
||||||
|
if (!key_mgr_hal_is_key_deployment_valid(key_config->key_type)) {
|
||||||
|
ESP_LOGE(TAG, "Key deployment is not valid");
|
||||||
|
// Todo - Define respective error code;
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Key deployment valid");
|
||||||
|
// Wait till Key Manager deployment is complete
|
||||||
|
key_mgr_hal_continue();
|
||||||
|
key_mgr_wait_for_state(ESP_KEY_MGR_STATE_IDLE);
|
||||||
|
//memcpy(&key_info->huk_recovery_info[0], huk_recovery_info, KEY_MGR_HUK_INFO_SIZE);
|
||||||
|
//memcpy(&key_info->key_recovery_info[0], key_recovery_info, KEY_MGR_KEY_RECOVERY_INFO_SIZE);
|
||||||
|
key_info->key_purpose = key_config->key_purpose;
|
||||||
|
free(key_recovery_info);
|
||||||
|
free(huk_recovery_info);
|
||||||
|
key_mgr_hal_set_key_usage(ESP_KEY_MGR_XTS_KEY, ESP_KEY_MGR_USE_OWN_KEY);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
93
components/esp_hw_support/include/esp_key_mgr.h
Normal file
93
components/esp_hw_support/include/esp_key_mgr.h
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "hal/key_mgr_types.h"
|
||||||
|
#include "hal/huk_types.h"
|
||||||
|
|
||||||
|
#define KEY_MGR_SW_INIT_KEY_SIZE 32
|
||||||
|
#define KEY_MGR_ASSIST_INFO_SIZE 64
|
||||||
|
#define KEY_MGR_KEY_RECOVERY_INFO_SIZE 64
|
||||||
|
#define KEY_MGR_HUK_INFO_SIZE 64
|
||||||
|
/* AES deploy mode */
|
||||||
|
#define KEY_MGR_K2_INFO_SIZE 64
|
||||||
|
#define KEY_MGR_K1_ENCRYPTED_SIZE 32
|
||||||
|
#define KEY_MGR_ECDH0_INFO_SIZE 64
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
// TODO - Should we use fixed arrays here instead of pointers ?
|
||||||
|
esp_key_mgr_key_type_t key_type;
|
||||||
|
uint8_t *huk_info;
|
||||||
|
size_t huk_info_size;
|
||||||
|
esp_key_mgr_key_purpose_t key_purpose;
|
||||||
|
bool use_sw_init_key;
|
||||||
|
uint8_t *sw_init_key;
|
||||||
|
size_t sw_init_key_size;
|
||||||
|
uint8_t k2_info[KEY_MGR_K2_INFO_SIZE];
|
||||||
|
uint8_t k1_encrypted[KEY_MGR_K1_ENCRYPTED_SIZE];
|
||||||
|
} esp_key_mgr_aes_key_config_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
// TODO - Should we use fixed arrays here instead of pointers ?
|
||||||
|
esp_key_mgr_key_type_t key_type;
|
||||||
|
uint8_t *huk_info;
|
||||||
|
size_t huk_info_size;
|
||||||
|
esp_key_mgr_key_purpose_t key_purpose;
|
||||||
|
bool use_sw_init_key;
|
||||||
|
uint8_t *sw_init_key;
|
||||||
|
size_t sw_init_key_size;
|
||||||
|
uint8_t k1_G[KEY_MGR_ECDH0_INFO_SIZE];
|
||||||
|
uint8_t k2_G[KEY_MGR_ECDH0_INFO_SIZE];
|
||||||
|
} esp_key_mgr_ecdh0_key_config_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t huk_recovery_info[KEY_MGR_HUK_INFO_SIZE];
|
||||||
|
uint8_t key_recovery_info[KEY_MGR_KEY_RECOVERY_INFO_SIZE];
|
||||||
|
esp_key_mgr_key_type_t key_type;
|
||||||
|
esp_key_mgr_key_purpose_t key_purpose;
|
||||||
|
} __attribute__((packed)) esp_key_mgr_key_recovery_info_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if the deployed key is valid or not
|
||||||
|
* @input
|
||||||
|
* key_config(input) AES key configuration
|
||||||
|
* key_info(output) A writable struct of esp_key_mgr_key_info_t type. The recovery key info for the deplyed key shall be stored here
|
||||||
|
* @return
|
||||||
|
* ESP_OK for success
|
||||||
|
* ESP_FAIL/relevant error code for failure
|
||||||
|
*/
|
||||||
|
esp_err_t esp_key_mgr_deploy_key_in_aes_mode(esp_key_mgr_aes_key_config_t *key_config, esp_key_mgr_key_recovery_info_t *key_info);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if the deployed key is valid or not
|
||||||
|
* @input
|
||||||
|
* key_config(input) AES key configuration
|
||||||
|
* key_info(output) A writable struct of esp_key_mgr_key_info_t type. The recovery key info for the deplyed key shall be stored here
|
||||||
|
* @return
|
||||||
|
* ESP_OK for success
|
||||||
|
* ESP_FAIL/relevant error code for failure
|
||||||
|
*/
|
||||||
|
esp_err_t esp_key_mgr_deploy_key_in_aes_mode(esp_key_mgr_aes_key_config_t *key_config, esp_key_mgr_key_recovery_info_t *key_info);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @brief Recover a key from the given key info
|
||||||
|
*
|
||||||
|
* @input
|
||||||
|
* key_info The key info required to recover the key
|
||||||
|
* @return
|
||||||
|
* ESP_OK for success
|
||||||
|
* ESP_FAIL/revevant error code for failure
|
||||||
|
*/
|
||||||
|
esp_err_t esp_key_mgr_recover_key(esp_key_mgr_key_recovery_info_t *key_recovery_info);
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
@@ -143,6 +143,10 @@ config SOC_ECDSA_SUPPORTED
|
|||||||
bool
|
bool
|
||||||
default y
|
default y
|
||||||
|
|
||||||
|
config SOC_KEY_MANAGER_SUPPORTED
|
||||||
|
bool
|
||||||
|
default y
|
||||||
|
|
||||||
config SOC_FLASH_ENC_SUPPORTED
|
config SOC_FLASH_ENC_SUPPORTED
|
||||||
bool
|
bool
|
||||||
default y
|
default y
|
||||||
|
@@ -60,7 +60,7 @@
|
|||||||
#define SOC_ECC_SUPPORTED 1
|
#define SOC_ECC_SUPPORTED 1
|
||||||
#define SOC_ECC_EXTENDED_MODES_SUPPORTED 1
|
#define SOC_ECC_EXTENDED_MODES_SUPPORTED 1
|
||||||
#define SOC_ECDSA_SUPPORTED 1
|
#define SOC_ECDSA_SUPPORTED 1
|
||||||
// #define SOC_KEY_MANAGER_SUPPORTED 1 //TODO: IDF-7925
|
#define SOC_KEY_MANAGER_SUPPORTED 1
|
||||||
#define SOC_FLASH_ENC_SUPPORTED 1
|
#define SOC_FLASH_ENC_SUPPORTED 1
|
||||||
#define SOC_SECURE_BOOT_SUPPORTED 1
|
#define SOC_SECURE_BOOT_SUPPORTED 1
|
||||||
// #define SOC_BOD_SUPPORTED 1 //TODO: IDF-7519
|
// #define SOC_BOD_SUPPORTED 1 //TODO: IDF-7519
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 |
|
||||||
|
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
|
||||||
|
|
||||||
# Peripherals Examples
|
# Peripherals Examples
|
||||||
|
|
||||||
This section provides examples how to configure and use ESP32’s internal peripherals like GPIO, UART, I2C, SPI, timers, counters, ADC / DAC, PWM, etc.
|
This section provides examples how to configure and use ESP32’s internal peripherals like GPIO, UART, I2C, SPI, timers, counters, ADC / DAC, PWM, etc.
|
||||||
|
Reference in New Issue
Block a user