Я пишу код C на STM32 (в частности, STM32756-EVAL), и я создал перечисление, которое считывает входящий массив символов и присваивает ему перечисление. Затем значение этого перечисления помещается в качестве индекса для массива указателей функций.
Причина, по которой у меня есть этот код, заключается в том, чтобы иметь возможность решить, какую функцию вызывать на основе полученного массива символов, не полагаясь на гигантский стек if-else, считывающий массивы символов один за другим.
Соответствующий код:
enum cmd cmd_Converter(unsigned char* inputCmd){//Converts the input cmd from uint8_t to an enum.
switch (inputCmd[0]){ //Currently we are using a switch-case. I expect this list to expand to something like 50.
case 'F':
if (memcmp(inputCmd, "FIRMV", COMMAND_LENGTH) == 0) return FIRMV;
else return INVAL;
break;
case 'V':
if (memcmp(inputCmd, "VALCN", COMMAND_LENGTH) == 0) return VALCN;
else return INVAL;
break;
default:
return INVAL;
}
}
void process_Message(uint8_t* message, uint16_t Len){
unsigned char inputCmd[COMMAND_LENGTH];
unsigned char inputData[DATA_LENGTH];
unsigned char outputCmd[COMMAND_LENGTH];
unsigned char outputData[DATA_LENGTH];
//Function that separates message, inputCmd, and inputMessage.
memcpy((char*) inputCmd, (const char*)message + COMMAND_CHAR, COMMAND_LENGTH);
memcpy((char*) inputData, (const char*)message + DATA_CHAR, DATA_LENGTH);
enum cmd enumCmd = cmd_Converter(inputCmd);
void (*cmd_Function_Pointer[])(unsigned char* inputData) = {FIRMV_F, VALCN_F, INVAL_F}; //Is this even needed?
(*cmd_Function_Pointer[enumCmd])(inputData);
// message_Received(message, Len);
// send_Message(outputCmd, outputData);
}
void FIRMV_F(unsigned char *inputData){
//Do thing
}
void VALCN_F(unsigned char *inputData){
//Do thing
}
void INVAL_F(unsigned char *inputData){
//Do thing
}
Перечисление предназначено для улучшения читаемости кода, так что любой, кто читает код, может увидеть перечисление и указатель на функцию и сказать: «enum FIRMV вызовет FIRMV_F из (*cmd_Function_Pointer[enumCmd])(inputData)». Одна из выявленных мной слабых сторон заключается в том, что он зависит от идентичности последовательности enum cmd и cmd_Function_Pointer[], и если список перечислений станет слишком длинным, поддерживать эту идентичную последовательность будет сложно.
Мне интересно, есть ли в C какие-либо методы, которые позволили бы «синхронизировать» идентификаторы в перечислении и именах функций, вызываемых указателем функции?
Полный код: usbd_cdc_if.c
/**
* @brief Data received over USB OUT endpoint are sent over CDC interface
* through this function.
*
* @note
* This function will issue a NAK packet on any OUT packet received on
* USB endpoint until exiting this function. If you exit this function
* before transfer is complete on CDC interface (ie. using DMA controller)
* it will result in receiving more data while previous ones are still
* not sent.
*
* @param Buf: Buffer of data to be received
* @param Len: Number of data received (in bytes)
* @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
*/
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
memset (buffer, '\0', 64); // clear the buffer
uint8_t len = (uint8_t)*Len; //Converts Len as uint32_t to len as uint8_t
memcpy(buffer, Buf, len); // copy the data to the buffer
memset(Buf, '\0', len); // clear the Buf also
//Code used to send message back
process_Message(buffer, len);
return (USBD_OK);
/* USER CODE END 6 */
}
/**
* @brief CDC_Transmit_FS
* Data to send over USB IN endpoint are sent over CDC interface
* through this function.
* @note
*
*
* @param Buf: Buffer of data to be sent
* @param Len: Number of data to be sent (in bytes)
* @retval USBD_OK if all operations are OK else USBD_FAIL or USBD_BUSY
*/
uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
{
uint8_t result = USBD_OK;
/* USER CODE BEGIN 7 */
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
if (hcdc->TxState != 0){ //If TxState in hcdc is not 0, return USBD_BUSY.
return USBD_BUSY;
}
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len); //SetTxBuffer sets the size of the buffer, as well as the buffer itself.
result = USBD_CDC_TransmitPacket(&hUsbDeviceFS); //USBD_CDC_TransmitPacket(&hUsbDeviceFS) transmits
/* USER CODE END 7 */
return result;
}
messageprocesser.c
#include "messageprocesser.h"
#include "main.h"
#include "usbd_cdc_if.h"
#include <string.h>
#include "string.h"
//Sample cmd: TOARM_FIRMV_00000000_4C\r\n
#define MESSAGE_LENGTH 25
#define COMMAND_CHAR 6 //See SW Protocol or sample cmd
#define COMMAND_LENGTH 5
#define DATA_CHAR 12
#define DATA_LENGTH 8
#define CHECKSUM_CHAR 21
#define CHECKSUM_LENGTH 2
enum cmd {FIRMV, VALCN, INVAL};
enum cmd cmd_Converter(unsigned char* inputCmd){//Converts the input cmd from uint8_t to an enum.
switch (inputCmd[0]){
case 'F':
if (memcmp(inputCmd, "FIRMV", COMMAND_LENGTH) == 0) return FIRMV;
else return INVAL;
break;
case 'V':
if (memcmp(inputCmd, "VALCN", COMMAND_LENGTH) == 0) return VALCN;
else return INVAL;
break;
default:
return INVAL;
}
}
void process_Message(uint8_t* message, uint16_t Len){
//HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
unsigned char inputCmd[COMMAND_LENGTH]; //These are not null-terminated strings.
unsigned char inputData[DATA_LENGTH]; //These are just an array of chars.
unsigned char outputCmd[COMMAND_LENGTH];
unsigned char outputData[DATA_LENGTH];
//Function that separates message, inputCmd, and inputMessage.
memcpy((char*) inputCmd, (const char*)message + COMMAND_CHAR, COMMAND_LENGTH);
memcpy((char*) inputData, (const char*)message + DATA_CHAR, DATA_LENGTH);
enum cmd enumCmd = cmd_Converter(inputCmd);
void (*cmd_Function_Pointer[])(unsigned char* inputData) = {FIRMV_F, VALCN_F, INVAL_F};
(*cmd_Function_Pointer[enumCmd])(inputData);
}
void FIRMV_F(unsigned char *inputData){
//HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
unsigned char outputCmd[COMMAND_LENGTH];
unsigned char outputData[DATA_LENGTH];
memcpy(outputCmd, "FIRMV", COMMAND_LENGTH);
memcpy(outputData, "01050A00", DATA_LENGTH);
send_Message(outputCmd, outputData);
}
void VALCN_F(unsigned char *inputData){
//HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
unsigned char outputCmd[COMMAND_LENGTH];
unsigned char outputData[DATA_LENGTH];
memcpy(outputCmd, "VALCN", COMMAND_LENGTH);
memcpy(outputData, "00000000", DATA_LENGTH);
send_Message(outputCmd, outputData);
}
void INVAL_F(unsigned char *inputData){
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
unsigned char outputCmd[COMMAND_LENGTH];
unsigned char outputData[DATA_LENGTH];
memcpy(outputCmd, "REEEE", COMMAND_LENGTH);
memcpy(outputData, "99999999", DATA_LENGTH);
send_Message(outputCmd, outputData);
}
void send_Message(uint8_t* cmd, uint8_t* data){
uint8_t outputMessage[MESSAGE_LENGTH] = "TOWST_";
memcpy((char*) outputMessage + COMMAND_CHAR, (const char*) cmd, COMMAND_LENGTH);
outputMessage[COMMAND_CHAR + COMMAND_LENGTH] = '_';
memcpy((char*) outputMessage + DATA_CHAR, (const char*) data, DATA_LENGTH);
outputMessage[DATA_CHAR + DATA_LENGTH] = '_';
//Deal with checksum
int outputCheckSum = checkSum_Generator(outputMessage);
char outputCheckSumHex[2] = {'0', '0'};
itoa (outputCheckSum, outputCheckSumHex, 16);
if (outputCheckSum < 16) { //Adds a 0 if CS has fewer than 2 numbers
outputCheckSumHex[1] = outputCheckSumHex[0];
outputCheckSumHex[0] = '0';
}
outputCheckSumHex[0] = toupper (outputCheckSumHex[0]);
outputCheckSumHex[1] = toupper (outputCheckSumHex[1]);
memcpy((char*) outputMessage + CHECKSUM_CHAR, (const char*) outputCheckSumHex, CHECKSUM_LENGTH);
outputMessage[23] = '\r';
outputMessage[24] = '\n';
//return a processed message array
CDC_Transmit_FS(outputMessage, sizeof(outputMessage));
}
int checkSum_Generator(uint8_t* message){
int checkSum = 0;
for (int i = 0; i < CHECKSUM_CHAR; i++){ //Gives the cs of TOARM_COMND_DATA0000_.
checkSum ^= message[i];
}
return checkSum;
}
Попытки решения этого вопроса
Еще один вопрос, связанный с «связыванием указателей функций и перечисления», который я изучил, но предложенное решение, похоже, не решает проблему, о которой я упоминал, а только обходит за счет меньшего кода.
До сих пор я пытался изменить имена функций, чтобы они были идентичны их аналогам перечисления, переименовав FIRMV_F() в FIRMV(). Неудивительно, что я получил это:
../Core/Src/messageprocesser.c:59:6: error: 'FIRMV' redeclared as different kind of symbol
Я также попытался присвоить массив указателей функций аналогично обычным массивам:
void process_Message(uint8_t* message, uint16_t Len){
//HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
unsigned char inputCmd[COMMAND_LENGTH]; //These are not null-terminated strings.
unsigned char inputData[DATA_LENGTH]; //These are just an array of chars.
unsigned char outputCmd[COMMAND_LENGTH];
unsigned char outputData[DATA_LENGTH];
//Function that separates message, inputCmd, and inputMessage.
memcpy((char*) inputCmd, (const char*)message + COMMAND_CHAR, COMMAND_LENGTH);
memcpy((char*) inputData, (const char*)message + DATA_CHAR, DATA_LENGTH);
enum cmd enumCmd = cmd_Converter(inputCmd);
void (*cmd_Function_Pointer[INVAL + 1])(unsigned char* inputData);
(*cmd_Function_Pointer[FIRMV]) = FIRMV_F;
(*cmd_Function_Pointer[VALCN]) = VALCN_F;
(*cmd_Function_Pointer[INVAL]) = INVAL_F;
(*cmd_Function_Pointer[enumCmd])(inputData); //What is going on here?
}
Я получил следующие ошибки.
../Core/Src/messageprocesser.c:52:33: error: lvalue required as left operand of assignment
52 | (*cmd_Function_Pointer[FIRMV]) = FIRMV_F;
| ^
../Core/Src/messageprocesser.c:53:33: error: lvalue required as left operand of assignment
53 | (*cmd_Function_Pointer[VALCN]) = VALCN_F;
| ^
../Core/Src/messageprocesser.c:54:33: error: lvalue required as left operand of assignment
54 | (*cmd_Function_Pointer[INVAL]) = INVAL_F;
Это имеет смысл, так как я понимаю, что функции FIRMV_F не имеют lvalue, но я не знаю, как это исправить, предполагая, что это возможно.
Пожалуйста, дайте мне знать, если требуется больше деталей или ясности.
Для менее решительных читателей: использование enum в качестве индекса в массиве указателей функций для удобства чтения. В текущем коде последовательность перечисления должна быть идентична последовательности указателя функции. Это кажется уязвимым. Хотите, чтобы последовательность перечисления оставалась идентичной последовательности указателя функции.
Моей первой попыткой было использовать гигантский стек if-else, преобразующий 50 строк в функции. Это выглядело довольно уродливо и яндре-деви, поэтому я поговорил с стековой биржей разработки программного обеспечения, которая предложила switch-case. Чтобы s-c работал, мне нужны были перечисления. Конечно, если есть строка для указателя на функцию, которая не является гигантским стеком if-else, я был бы рад услышать об этом.
Как насчет подхода X Macro в последних ответах на вопрос, который вы связали?
(*cmd_Function_Pointer[FIRMV])
должно быть cmd_Function_Pointer[FIRMV]
Макросы @nielsen X должны быть последним средством. Очень полезно при сопровождении старого хлама, в котором мало что можно изменить, но я не рекомендую использовать их в новых программах. Тем не менее, для полноты картины я разместил здесь версию своего ответа для X-макроса.
Вы также можете написать ... = {[FIRMV] = FIRMV_F, [VALCN] = VALCN_F, ...}
Вы можете сделать так, чтобы ваша функция cmd_Converter() просто возвращала требуемый указатель функции. Вы по-прежнему можете использовать оператор switch, чтобы избавиться от необходимости иметь длинную цепочку if...else if.... Таким образом, вам не нужны ни перечисления, ни массивы указателей на функции, и нечего синхронизировать.
#include <string.h>
#include <stdlib.h>
#define COMMAND_LENGTH 8
typedef void (*handler_t)(const unsigned char *arg);
void FIRMV_F(const unsigned char *inputData);
handler_t cmd_Converter(const unsigned char *inputCmd)
{
handler_t result = NULL;
switch (inputCmd[0])
{
case 'F':
if (memcmp(inputCmd, "FIRMV", COMMAND_LENGTH) == 0)
result = FIRMV_F;
break;
// more cases here.
default:
break;
}
return result;
}
Редактировать: Если вам нужен чисто управляемый данными подход, который полностью избегает оператора switch, вы можете иметь массив структур, отображающих имена команд на указатели функций. Это по-прежнему устраняет необходимость в перечислениях — отображение явное:
#include <string.h>
#include <stdlib.h>
#define COMMAND_LENGTH 8
typedef void (*handler_t)(const unsigned char *arg);
typedef struct
{
const char *name;
handler_t fn_p;
} cmd_t;
void FIRMV_F(const unsigned char *inputData);
void VALCN_F(const unsigned char *inputData);
static const cmd_t commands[] =
{
{"FIRMV", FIRMV_F},
{"VALCN", VALCN_F}
};
handler_t cmd_Converter(const unsigned char *inputCmd)
{
handler_t result = NULL;
for (size_t i = 0; i < sizeof commands / sizeof commands[0]; ++i)
{
if (strcmp((const char *)inputCmd, commands[i].name) == 0)
{
result = commands[0].fn_p;
break;
}
}
return result;
}
Для этого используется линейный поиск. Если у вас не так много команд, этого может быть достаточно. Если их много, вы можете отсортировать массив и выполнить двоичный поиск.
Тем не менее, вам все равно придется поддерживать большой старый switch
со всеми ветвлениями, которые он генерирует. Создание пар перечисление/функция — довольно разумный и полезный дизайн для многих сценариев, например, при создании конечных автоматов.
@ Лундин Согласен. Если бы я делал это, у меня был бы массив структур, содержащих имя команды и указатель функции, и поиск по нему. Но я отвечал на заданный вопрос.
@Lundin Обновлено, чтобы добавить версию, управляемую данными, без переключателя.
Structs — это нормальное решение, оно удобочитаемо и имеет хороший дизайн, но преимущество наличия таблицы указателей enum и функций заключается в том, что вы получаете прямой доступ. Никаких циклов, никаких переключателей, никакого поиска или strcmp, только просмотр таблицы. Что касается вашего примера здесь, его нужно переписать, используя bsearch
, когда общее количество элементов составляет около 50+.
@Lundin Если они изменят свой дизайн, чтобы получать номер через USB, это сработает. Как бы то ни было, они получают строку через USB. Где-то должен быть strcmp()
или что-то подобное.
Существует распространенная рекомендуемая практика, предполагающая, что значения перечисления просто последовательны. Сделайте перечисление следующим образом:
typedef enum
{
INVAL,
FIRMV,
VALCN,
CMD_N // number of items in the enum
} cmd_t;
И такой шаблон функции:
typedef void cmd_func_t (unsigned char *inputData);
Затем вы можете создать массив указателей на функции, где каждый индекс соответствует соответствующему перечислению, используя назначенные инициализаторы:
static cmd_func_t* const cmd_list[] = // size not specified on purpose
{
[INVAL] = INVAL_F,
[FIRMV] = FIRMV_F,
[VALCN] = VALCN_F,
};
Проверьте целостность с помощью:
_Static_assert(sizeof cmd_list/sizeof cmd_list[0] == CMD_N,
"cmd_list has wrong size compared to cmd_t");
Использование вызова функции, например:
cmd_list[FIRMV](param);
Кроме того, просто для полноты картины мы можем полностью отказаться от «не повторяться» и сгенерировать многое из этого с помощью X-макросов: https://godbolt.org/z/zY1nh5M5T. На самом деле не рекомендуется, так как это делает код неясным, но довольно мощным. Например, такие строки, как "FIRMV"
, могут генерироваться во время компиляции, как показано в этом примере.
Хотя X-макросы в этом случае излишни, есть вопрос масштаба. Если бы мы говорили о 50+ элементах, я бы рассмотрел подход X-macro и написал объяснение в коде. Кстати, _Static_assert
не нужен в версии X-macro.
У вас есть двухэтапный процесс, который преобразует строку в перечисление, а затем перечисление в указатель на функцию. Было бы проще просто преобразовать строку в указатель функции (в
cmd_Converter()
). Просто идея.