mirror of
https://github.com/espressif/esp-idf.git
synced 2026-04-27 19:13:21 +00:00
Merge branch 'feat/async_crc_example' into 'master'
feat(crc): added async crc console example (your terminal CRC calculator 🧮) See merge request espressif/esp-idf!46084
This commit is contained in:
@@ -332,6 +332,11 @@ The ``dma_burst_size`` affects DMA transfer efficiency:
|
||||
|
||||
The optimal value depends on your chip's DMA controller capabilities.
|
||||
|
||||
Application Examples
|
||||
====================
|
||||
|
||||
- :example:`peripherals/dma/async_crc` demonstrates how to use the Async CRC driver through an interactive console CLI.
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
|
||||
@@ -332,6 +332,11 @@ DMA 突发大小
|
||||
|
||||
最佳值取决于芯片的 DMA 控制器功能。
|
||||
|
||||
应用示例
|
||||
========
|
||||
|
||||
- :example:`peripherals/dma/async_crc` 演示了如何通过交互式控制台 CLI 使用异步 CRC 驱动程序。
|
||||
|
||||
API 参考
|
||||
========
|
||||
|
||||
|
||||
@@ -95,6 +95,13 @@ examples/peripherals/dac/dac_cosine_wave:
|
||||
- esp_driver_dac
|
||||
- soc
|
||||
|
||||
examples/peripherals/dma/async_crc:
|
||||
disable:
|
||||
- if: SOC_GDMA_SUPPORT_CRC != 1
|
||||
depends_components:
|
||||
- esp_driver_dma
|
||||
- esp_console
|
||||
|
||||
examples/peripherals/gpio:
|
||||
depends_components:
|
||||
- esp_driver_gpio
|
||||
|
||||
@@ -151,6 +151,4 @@ I (8291) sensor_init: Format in use:DVP_8bit_20Minput_RGB565_640x480_6fps
|
||||
|
||||
## Reference
|
||||
|
||||
- Link to the ESP-IDF feature's API reference, for example [ESP-IDF: Camera Controller Driver](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/camera_driver.html)
|
||||
- [ESP-IDF Getting Started](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html#get-started)
|
||||
- [Project Configuration](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/kconfig.html) (Kconfig Options)
|
||||
- [ESP-IDF: Camera Controller Driver](https://docs.espressif.com/projects/esp-idf/en/latest/esp32p4/api-reference/peripherals/camera_driver.html)
|
||||
|
||||
@@ -151,6 +151,4 @@ I (2609) sensor_init: Format in use:DVP_8bit_20Minput_RAW8_1024x600_15fps
|
||||
|
||||
## Reference
|
||||
|
||||
- Link to the ESP-IDF feature's API reference, for example [ESP-IDF: Camera Controller Driver](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/camera_driver.html)
|
||||
- [ESP-IDF Getting Started](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html#get-started)
|
||||
- [Project Configuration](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/kconfig.html) (Kconfig Options)
|
||||
- [ESP-IDF: Camera Controller Driver](https://docs.espressif.com/projects/esp-idf/en/latest/esp32p4/api-reference/peripherals/camera_driver.html)
|
||||
|
||||
@@ -134,6 +134,4 @@ I (408) dvp_spi_lcd: Screen lit up now!
|
||||
|
||||
## Reference
|
||||
|
||||
- Link to the ESP-IDF feature's API reference, for example [ESP-IDF: Camera Controller Driver](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/camera_driver.html)
|
||||
- [ESP-IDF Getting Started](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html#get-started)
|
||||
- [Project Configuration](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/kconfig.html) (Kconfig Options)
|
||||
- [ESP-IDF: Camera Controller Driver](https://docs.espressif.com/projects/esp-idf/en/latest/esp32p4/api-reference/peripherals/camera_driver.html)
|
||||
|
||||
@@ -152,6 +152,4 @@ This image is also used as a reference, you can check output image after ISP aut
|
||||
|
||||
## Reference
|
||||
|
||||
- Link to the ESP-IDF feature's API reference, for example [ESP-IDF: Camera Controller Driver](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/camera_driver.html)
|
||||
- [ESP-IDF Getting Started](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html#get-started)
|
||||
- [Project Configuration](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/kconfig.html) (Kconfig Options)
|
||||
- [ESP-IDF: Camera Controller Driver](https://docs.espressif.com/projects/esp-idf/en/latest/esp32p4/api-reference/peripherals/camera_driver.html)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(async_crc)
|
||||
@@ -0,0 +1,69 @@
|
||||
| Supported Targets | ESP32-P4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# Async CRC Console Example
|
||||
|
||||
(See the README.md file in the upper level 'examples' directory for more information about examples.)
|
||||
|
||||
## Overview
|
||||
|
||||
This example demonstrates how to use the Async CRC driver (`esp_async_crc.h`) through an interactive console CLI. The Async CRC driver provides hardware-accelerated CRC calculation using the General DMA (GDMA) peripheral.
|
||||
|
||||
The example provides a `crc` command that allows users to:
|
||||
- Calculate CRC-8, CRC-16, and CRC-32 checksums
|
||||
- Customize CRC parameters (polynomial, initial value, final XOR, bit reversal)
|
||||
- Test various CRC algorithms interactively
|
||||
|
||||
## Hardware Required
|
||||
|
||||
Any board with a supported ESP target that mentioned in the above table can be used.
|
||||
|
||||
## Build and Flash
|
||||
|
||||
Run `idf.py -p PORT flash monitor` to build and flash the project.
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects.
|
||||
|
||||
## Example Output
|
||||
|
||||
### Check help
|
||||
|
||||
```bash
|
||||
crc> help
|
||||
crc [--width=<width>] [--poly=<hex>] [--init=<hex>] [--xor=<hex>] [--reverse-input=<0|1>] [--reverse-output=<0|1>] <data>
|
||||
Calculate CRC checksum using hardware async CRC driver
|
||||
--width=<width> CRC width (8, 16, or 32). Default: 8
|
||||
--poly=<hex> CRC polynomial in hex. Default: 0x07
|
||||
--init=<hex> Initial CRC value in hex. Default: 0x00
|
||||
--xor=<hex> Final XOR value in hex. Default: 0x00
|
||||
--reverse-input=<0|1> Reverse input bits (0 or 1). Default: 0
|
||||
--reverse-output=<0|1> Reverse output bits (0 or 1). Default: 0
|
||||
<data> Input data string
|
||||
```
|
||||
|
||||
### Calculate CRC-8 (default)
|
||||
|
||||
```bash
|
||||
crc> crc "test"
|
||||
CRC result: 0xB9
|
||||
```
|
||||
|
||||
### Calculate CRC-16/CCITT
|
||||
|
||||
```bash
|
||||
crc> crc --width 16 --poly 0x1021 "test"
|
||||
CRC result: 0x9B06
|
||||
```
|
||||
|
||||
### Calculate CRC-32
|
||||
|
||||
```bash
|
||||
crc> crc --width 32 --poly 0x04C11DB7 --init 0xFFFFFFFF --xor 0xFFFFFFFF --reverse-input 1 --reverse-output 1 "test"
|
||||
CRC result: 0xD87F7E0C
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.)
|
||||
@@ -0,0 +1,4 @@
|
||||
idf_component_register(SRCS "async_crc_example_main.c"
|
||||
"cmd_crc.c"
|
||||
PRIV_REQUIRES console esp_driver_dma
|
||||
INCLUDE_DIRS ".")
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_console.h"
|
||||
#include "cmd_crc.h"
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_console_repl_t *repl = NULL;
|
||||
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
|
||||
repl_config.prompt = "crc>";
|
||||
|
||||
esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl));
|
||||
|
||||
// initialize the CRC engine and register the CRC command
|
||||
register_crc();
|
||||
|
||||
printf("\n ==============================================================\n");
|
||||
printf(" Steps to Use CRC \n");
|
||||
printf(" \n");
|
||||
printf(" 1. Try 'help', check all supported commands \n");
|
||||
printf(" 2. Try 'crc \"test\"' to calculate the default 8-bit CRC \n");
|
||||
printf(" 3. Or customize: 'crc --width=16 --poly=0x1021 \"test\"' \n");
|
||||
printf(" \n");
|
||||
printf(" ==============================================================\n\n");
|
||||
|
||||
ESP_ERROR_CHECK(esp_console_start_repl(repl));
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include "argtable3/argtable3.h"
|
||||
#include "esp_console.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_async_crc.h"
|
||||
#include "cmd_crc.h"
|
||||
|
||||
static const char *TAG = "crc";
|
||||
|
||||
static async_crc_handle_t s_crc_handle = NULL;
|
||||
|
||||
static struct {
|
||||
struct arg_int *width; /*!< CRC width (8, 16, or 32) */
|
||||
struct arg_str *poly; /*!< CRC polynomial (hex string) */
|
||||
struct arg_str *init; /*!< Initial CRC value (hex string) */
|
||||
struct arg_str *xor; /*!< Final XOR value (hex string) */
|
||||
struct arg_int *reverse_input; /*!< Reverse input bits */
|
||||
struct arg_int *reverse_output; /*!< Reverse output bits */
|
||||
struct arg_str *data; /*!< Input data string */
|
||||
struct arg_end *end; /*!< argtable end marker */
|
||||
} crc_args;
|
||||
|
||||
static uint32_t parse_hex_value(const char *str)
|
||||
{
|
||||
uint32_t value = 0;
|
||||
if (str && str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) {
|
||||
value = (uint32_t)strtoul(str, NULL, 16);
|
||||
} else {
|
||||
value = (uint32_t)strtoul(str, NULL, 0);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static int do_crc_cmd(int argc, char **argv)
|
||||
{
|
||||
int nerrors = arg_parse(argc, argv, (void **)&crc_args);
|
||||
if (nerrors != 0) {
|
||||
arg_print_errors(stderr, crc_args.end, argv[0]);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (!s_crc_handle) {
|
||||
ESP_LOGE(TAG, "CRC driver not initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// By default, calculate an 8-bit CRC with polynomial 0x07, initial value 0x00, and final XOR value 0x00, without bit reversal
|
||||
async_crc_params_t params = {
|
||||
.width = 8,
|
||||
.polynomial = 0x07,
|
||||
.init_value = 0x00,
|
||||
.final_xor_value = 0x00,
|
||||
.reverse_input = false,
|
||||
.reverse_output = false,
|
||||
};
|
||||
|
||||
if (crc_args.width->count) {
|
||||
int width = crc_args.width->ival[0];
|
||||
if (width != 8 && width != 16 && width != 32) {
|
||||
ESP_LOGE(TAG, "Invalid width %d. Must be 8, 16, or 32", width);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
params.width = width;
|
||||
}
|
||||
|
||||
if (crc_args.poly->count) {
|
||||
params.polynomial = parse_hex_value(crc_args.poly->sval[0]);
|
||||
}
|
||||
|
||||
if (crc_args.init->count) {
|
||||
params.init_value = parse_hex_value(crc_args.init->sval[0]);
|
||||
}
|
||||
|
||||
if (crc_args.xor->count) {
|
||||
params.final_xor_value = parse_hex_value(crc_args.xor->sval[0]);
|
||||
}
|
||||
|
||||
if (crc_args.reverse_input->count) {
|
||||
params.reverse_input = crc_args.reverse_input->ival[0] ? true : false;
|
||||
}
|
||||
|
||||
if (crc_args.reverse_output->count) {
|
||||
params.reverse_output = crc_args.reverse_output->ival[0] ? true : false;
|
||||
}
|
||||
|
||||
const char *data_str = crc_args.data->sval[0];
|
||||
size_t data_len = strlen(data_str);
|
||||
|
||||
uint32_t result = 0;
|
||||
esp_err_t err = esp_crc_calc_blocking(s_crc_handle, data_str, data_len, ¶ms, -1, &result);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "CRC calculation failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
if (params.width == 8) {
|
||||
printf("CRC result: 0x%02" PRIX32 "\n", result);
|
||||
} else if (params.width == 16) {
|
||||
printf("CRC result: 0x%04" PRIX32 "\n", result);
|
||||
} else {
|
||||
printf("CRC result: 0x%08" PRIX32 "\n", result);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void register_crc(void)
|
||||
{
|
||||
async_crc_config_t config = {
|
||||
.backlog = 1, // backlog of 1 is sufficient since we wait for each calculation to finish before starting the next one
|
||||
.dma_burst_size = 32,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_async_crc_install_gdma_ahb(&config, &s_crc_handle));
|
||||
|
||||
crc_args.width = arg_int0(NULL, "width", "<width>", "CRC width (8, 16, or 32). Default: 8");
|
||||
crc_args.poly = arg_str0(NULL, "poly", "<hex>", "CRC polynomial in hex. Default: 0x07");
|
||||
crc_args.init = arg_str0(NULL, "init", "<hex>", "Initial CRC value in hex. Default: 0x00");
|
||||
crc_args.xor = arg_str0(NULL, "xor", "<hex>", "Final XOR value in hex. Default: 0x00");
|
||||
crc_args.reverse_input = arg_int0(NULL, "reverse-input", "<0|1>", "Reverse input bits (0 or 1). Default: 0");
|
||||
crc_args.reverse_output = arg_int0(NULL, "reverse-output", "<0|1>", "Reverse output bits (0 or 1). Default: 0");
|
||||
crc_args.data = arg_str1(NULL, NULL, "<data>", "Input data string");
|
||||
crc_args.end = arg_end(7);
|
||||
|
||||
const esp_console_cmd_t crc_cmd = {
|
||||
.command = "crc",
|
||||
.help = "Calculate CRC checksum using hardware async CRC driver",
|
||||
.hint = NULL,
|
||||
.func = &do_crc_cmd,
|
||||
.argtable = &crc_args
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_console_cmd_register(&crc_cmd));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void register_crc(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,75 @@
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
|
||||
import pytest
|
||||
from crc import Calculator
|
||||
from crc import Configuration
|
||||
from pytest_embedded_idf import IdfDut
|
||||
from pytest_embedded_idf.utils import idf_parametrize
|
||||
from pytest_embedded_idf.utils import soc_filtered_targets
|
||||
|
||||
EXPECT_TIMEOUT = 20
|
||||
|
||||
|
||||
def calculate_crc8(data: str) -> int:
|
||||
config = Configuration(8, 0x07, init_value=0x00, final_xor_value=0x00, reverse_input=False, reverse_output=False)
|
||||
calculator = Calculator(config)
|
||||
return int(calculator.checksum(data.encode('utf-8')))
|
||||
|
||||
|
||||
def calculate_crc16_ccitt(data: str) -> int:
|
||||
config = Configuration(
|
||||
16, 0x1021, init_value=0x0000, final_xor_value=0x0000, reverse_input=False, reverse_output=False
|
||||
)
|
||||
calculator = Calculator(config)
|
||||
return int(calculator.checksum(data.encode('utf-8')))
|
||||
|
||||
|
||||
def calculate_crc32(data: str) -> int:
|
||||
config = Configuration(
|
||||
32, 0x04C11DB7, init_value=0xFFFFFFFF, final_xor_value=0xFFFFFFFF, reverse_input=True, reverse_output=True
|
||||
)
|
||||
calculator = Calculator(config)
|
||||
return int(calculator.checksum(data.encode('utf-8')))
|
||||
|
||||
|
||||
def calculate_crc32_no_reflect(data: str) -> int:
|
||||
config = Configuration(
|
||||
32, 0x04C11DB7, init_value=0x00000000, final_xor_value=0x0000, reverse_input=False, reverse_output=False
|
||||
)
|
||||
calculator = Calculator(config)
|
||||
return int(calculator.checksum(data.encode('utf-8')))
|
||||
|
||||
|
||||
@pytest.mark.generic
|
||||
@idf_parametrize('target', soc_filtered_targets('SOC_GDMA_SUPPORT_CRC == 1'), indirect=['target'])
|
||||
def test_async_crc_example(dut: IdfDut) -> None:
|
||||
dut.expect_exact('crc>', timeout=EXPECT_TIMEOUT)
|
||||
|
||||
# Test CRC-8 (default)
|
||||
dut.write('crc "test"')
|
||||
expected_crc8 = calculate_crc8('test')
|
||||
dut.expect(f'CRC result: 0x{expected_crc8:02X}', timeout=EXPECT_TIMEOUT)
|
||||
|
||||
# Test CRC-8 with explicit polynomial
|
||||
dut.write('crc --width 8 --poly 0x07 "test"')
|
||||
expected_crc8 = calculate_crc8('test')
|
||||
dut.expect(f'CRC result: 0x{expected_crc8:02X}', timeout=EXPECT_TIMEOUT)
|
||||
|
||||
# Test CRC-16/CCITT
|
||||
dut.write('crc --width 16 --poly 0x1021 "test"')
|
||||
expected_crc16 = calculate_crc16_ccitt('test')
|
||||
dut.expect(f'CRC result: 0x{expected_crc16:04X}', timeout=EXPECT_TIMEOUT)
|
||||
|
||||
# Test CRC-32 without reflection
|
||||
dut.write('crc --width 32 --poly 0x04C11DB7 --init 0 --xor 0 --reverse-input 0 --reverse-output 0 "test"')
|
||||
expected_crc32 = calculate_crc32_no_reflect('test')
|
||||
dut.expect(f'CRC result: 0x{expected_crc32:08X}', timeout=EXPECT_TIMEOUT)
|
||||
|
||||
# Test CRC-32 with reflection (standard CRC-32)
|
||||
dut.write(
|
||||
'crc --width 32 --poly 0x04C11DB7 --init 0xFFFFFFFF --xor 0xFFFFFFFF '
|
||||
'--reverse-input 1 --reverse-output 1 "test"'
|
||||
)
|
||||
expected_crc32 = calculate_crc32('test')
|
||||
dut.expect(f'CRC result: 0x{expected_crc32:08X}', timeout=EXPECT_TIMEOUT)
|
||||
@@ -203,7 +203,5 @@ This image is also used as a reference, you can check output image without ISP a
|
||||
|
||||
## Reference
|
||||
|
||||
- Link to the ESP-IDF camera controller driver API reference, [ESP-IDF: Camera Controller Driver](https://docs.espressif.com/projects/esp-idf/en/latest/esp32p4/api-reference/peripherals/camera_driver.html)
|
||||
- Link to the ESP-IDF ISP driver API reference, [ESP-IDF: Image Signal Processor](https://docs.espressif.com/projects/esp-idf/en/latest/esp32p4/api-reference/peripherals/isp.html)
|
||||
- [ESP-IDF Getting Started](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html#get-started)
|
||||
- [Project Configuration](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/kconfig.html) (Kconfig Options)
|
||||
- [ESP-IDF: Camera Controller Driver](https://docs.espressif.com/projects/esp-idf/en/latest/esp32p4/api-reference/peripherals/camera_driver.html)
|
||||
- [ESP-IDF: Image Signal Processor](https://docs.espressif.com/projects/esp-idf/en/latest/esp32p4/api-reference/peripherals/isp.html)
|
||||
|
||||
@@ -123,6 +123,4 @@ I (10085) main_task: Returned from app_main()
|
||||
|
||||
## Reference
|
||||
|
||||
- Link to the ESP-IDF feature's API reference, for example [ESP-IDF: Pixel-Processing Accelerator](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/ppa.html)
|
||||
- [ESP-IDF Getting Started](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html#get-started)
|
||||
- [Project Configuration](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/kconfig.html) (Kconfig Options)
|
||||
- [ESP-IDF: Pixel-Processing Accelerator](https://docs.espressif.com/projects/esp-idf/en/latest/esp32p4/api-reference/peripherals/ppa.html)
|
||||
|
||||
@@ -18,3 +18,6 @@ pyecharts
|
||||
|
||||
# for twai tests, communicate with socket can device (e.g. Canable)
|
||||
python-can
|
||||
|
||||
# Calculate CRC checksums
|
||||
crc
|
||||
|
||||
Reference in New Issue
Block a user