C: минимизация дублирования кода с использованием функций в заголовочном файле

Это немного странный случай использования, поэтому поиск существующего обсуждения затруднен. Я программирую для встраиваемых систем (Microchip PIC24 с использованием компилятора XC16) и в настоящее время реализую протокол связи одинаково по 3 отдельным каналам UART (каждый UART будет получать данные из таблицы основных данных).

Я начал писать проект так, чтобы каждый UART обрабатывался отдельным модулем с большим количеством дублирования кода в соответствии со следующим псевдокодом:

UART1.c:

static unsigned char buffer[128];
static unsigned char pointer = 0;
static unsigned char packet_received = 0;

void interrupt UART1Receive (void) {
    buffer[pointer++] = UART1RX_REG;
    if (end of packet condition) packet_received = 1;
}

void processUART1(void) {    // This is called regularly from main loop
    if (packet_received) {
        // Process packet
    }
}

UART2.c:

static unsigned char buffer[128];
static unsigned char pointer = 0;
static unsigned char packet_received = 0;

void interrupt UART2Receive (void) {
    buffer[pointer++] = UART2RX_REG;
    if (end of packet condition) packet_received = 1;
}

void processUART2(void) {    // This is called regularly from main loop
    if (packet_received) {
        // Process packet
    }
}

Несмотря на то, что вышесказанное аккуратно и хорошо работает, на практике сам протокол связи довольно сложен, поэтому дублирование его три раза (просто с изменениями в ссылках на регистры UART) увеличивает вероятность появления ошибок. Наличие единственной функции и передача указателей на нее не вариант, так как это будет иметь слишком большое влияние на скорость. Код должен быть физически продублирован в памяти для каждого UART.

Я много думал об этом и, несмотря на то, что знал правила, запрещающие помещать функции в заголовочный файл, решил попробовать специальный заголовочный файл, содержащий дублированный код, со ссылками в виде #defined values:

protocol.h:

// UART_RECEIVE_NAME and UART_RX_REG are just macros to be defined 
// in calling file
void interrupt UART_RECEIVE_NAME (void) {
    buffer[pointer++] = UART_RX_REG;
    if (end of packet condition) packet_received = 1;
}

UART1.c:

static unsigned char buffer[128];
static unsigned char pointer = 0;
static unsigned char packet_received = 0;

#define UART_RECEIVE_NAME UART1Receive
#define UART_RX_REG       UART1RX_REG

#include "protocol.h"

void processUART1(void) {    // This is called regularly from main loop
    if (packet_received) {
        // Process packet
    }
}

UART2.c:

static unsigned char buffer[128];
static unsigned char pointer = 0;
static unsigned char packet_received = 0;

#define UART_RECEIVE_NAME UART2Receive
#define UART_RX_REG       UART2RX_REG

#include "protocol.h"

void processUART2(void) {    // This is called regularly from main loop
    if (packet_received) {
        // Process packet
    }
}

Я был немного удивлен, когда код скомпилирован без ошибок! Похоже, что это работает, и после компиляции MPLAB X может даже обработать все ссылки на символы, чтобы каждая ссылка на макрос в UART1.c и UART2.c не была идентифицирована как неразрешимый идентификатор. Тогда я понял, что, вероятно, мне следует переименовать файл protocol.h в protocol.c (и соответственно обновить #include), но это не имеет большого значения.

Есть только один недостаток: IDE не знает, что делать при пошаговом выполнении кода, включенного в protocol.h, во время симуляции или отладки. Он просто остается в команде вызова во время выполнения кода, поэтому отладка будет немного сложнее.

Итак, насколько хакерское решение? Будут ли боги Си поразить меня за то, что я обдумал это? Есть ли лучшие альтернативы, которые я пропустил?

2 ответа

Альтернативой является определение макроса функции, который содержит тело кода. Некоторые операторы вставки токенов могут автоматически генерировать необходимые имена символов. Многострочные макросы могут быть созданы с помощью \ в конце всего, кроме последней строки.

#define UART_RECEIVE(n) \
void interrupt UART##n##Receive (void) { \
    buffer[pointer++] = UART##n##RX_REG; \
    if (end of packet condition) packet_received = 1; \
}

UART_RECEIVE(1)
UART_RECEIVE(2)

Использование макросов для этой цели кажется мне плохой идеей. Невозможность отладки является лишь одним недостатком. Это также затрудняет понимание, скрывая реальное значение символов. А подпрограммы прерывания должны быть независимыми и короткими, а общие функции скрыты в функциях-обработчиках.

Первое, что я хотел бы сделать, это определить общую структуру буфера для каждого UART. Это делает возможным одновременное общение. Если для каждого uart требуется отдельная функция-обработчик сообщений, ее можно включить в качестве указателя на функцию. Синтаксис немного сложен, но в результате получается эффективный код.

typedef struct uart_buf uart_buf_t;
struct uart_buf {
    uint8_t* buffer;
    int16_t  inptr;
    bool packet_received;
    void (*handler_func)(uart_buf_t*);
};

uart_buf_t uart_buf_1;
uart_buf_t uart_buf_2;

Тогда каждый обработчик прерываний будет выглядеть так:

void interrupt UART1Receive (void) {
  handle_input(UART1RX_REG, &uart_buf_1);
}

void interrupt UART2Receive (void) {
  handle_input(UART2RX_REG, &uart_buf_2);
}

И общий обработчик будет:

void handle_input(uint8_t in_char, *buff) {
   buf->buffer[buf->inptr++] = in_char; 
   if (in_char=LF) 
       buf->packet_received = true;
       buf->handler_func(buf);
   }
}

И обработчик сообщений:

void hadle_packet(uart_buf_t* buf) {
    ... code to handle message
    buf->packet_received=0;
}

И указатели на функции должны быть инициализированы:

void init() {
    uart_buf_1.handler_func=handler1;
    uart_buf_2.handler_func=handler1;
}

Полученный код очень гибкий и может быть легко изменен. Одноступенчатый код не проблема.

Другие вопросы по тегам