Указатели и массивы

В главе про архитектуру ARM рассматривалась карта памяти — по сути всё, что есть в микроконтроллере, имеет свой адрес, будь то регистры периферии, оперативная или flash-память, а сама карта памяти не что иное, как «массив» из ячеек (байт). Для работы с адресами необходима еще одна сущность — указатель.

Указатель

Указатель — это переменная, хранящая адрес другой переменной. Для получения адреса используется унарная операция &, которая неприменима к регистровым переменным. Унарная операция * называется операцией разыменования (англ. dereferencing) и возвращает значение, лежащее по адресу.

char ch   = 'A'; // ch = 65
char *ptr = &ch; // ptr = 0x080000000

Графически всё это выглядит так (все значения адресов случайны):

Обе операции имеют более высокий приоритет, чем арифметические. Например,

char b = *ptr + 1

запишет в переменную b значение по адресу ptr и прибавит к нему 1 (т.е. получится B, код 66). Если же указать скобками приоритет, то результат получится согласно адресной арифметике, и мы получим значение из соседней ячейки.

char b2 = *(ptr + 1); // 0x08000000 + 0x000000001

Это нормальная операция, если речь идет о массиве, но совершенно неверная, если программист не знает, что хранится в следующей ячейке. О массивах будет подробно рассказано позже, а сейчас перейдем к указателям.

Операция разыменования и адресная арифметика возможны для всех типов данных (char, int, float и т.д.), кроме void. Дело в том, что void ничего не говорит о размере и типе данных, поэтому невозможно получить значение, хранящееся по адресу: непонятно, сколько байт необходимо взять. То же касается адресной арифметики — непонятно, сколько байт нужно проскочить, чтобы получить следующий элемент. Для чего же нужен указатель на ничего?

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

/**
 * \brief This function swaps variables of any type.
 * \param[out] a becomes to b
 * \param[out] b becomes to a
 * \param[in] size data size
 */ 
void swap(void *a, void *b, size_t size) {
    uint8_t tmp; // temporary variavle, holds one byte
    size_t i;    // index
    for (i = 0; i < size; i++) {
        tmp = *((uint8_t*) b + i);
        *((uint8_t*) b + i) = *((char*) a + i);
    *((uint8_t*) a + i) = tmp;
    }
}

В таком случае воспользоваться функцией можно так:

int a = 0, b = 1;
float d = 15.01f, c = 4.3f;
swap(&a, &b, sizeof(a)); // function sizeof() located in STD library
swap(&c, &d, sizeof(c));

Другой способ использовать указатель на void — создать указатель на функцию. Это полезно в тех случаях, когда необходимо запустить программу из оперативной памяти или организовать перепрошивку устройства.

Рассмотрим второй сценарий — под микроконтроллер пишется маленькая программка, которую называют загрузчиком (англ. bootloader), которая записывает, скажем, с внешнего носителя (flash-карты) прошивку в память микроконтроллера, переносит таблицу векторов прерывания и далее передает управление функции main самой прошивки устройства. Пример использования приведен ниже:

inline void run_application(void) {
uint32_t application_address; // variable for main function address
void (*go_to_app_main)(void); // declaration of application main function
​
    // set start adress of application (user firmware)
    application_address = *((volatile uint32_t*) (FLASH_APP_START_ADDRESS + 4));
​
    go_to_app_main = (void (*)(void)) application_address;
    SCB->VTOR = FLASH_APP_START_ADDRESS; // vector table transffering
    __set_MSP(*((volatile u32*) FLASH_APP_START_ADDRESS));
    go_to_app_main(); // jump to application main function
}

До этого момента, говоря про указатели, мы работали с переменными, но что насчет периферии? Для примера рассмотрим работу с портом ввода-вывода GPIOA в микроконтроллере. Ниже приведена выдержка из библиотеки CMSIS (вендор-зависимой части, stm32f10x.h):

// Peripheral base address in the alias region
#define PERIPH_BASE           ((uint32_t)0x40000000)
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)
#define GPIOA_BASE            (APB2PERIPH_BASE + 0x0800)
// ...
#define GPIOA                 ((GPIO_TypeDef *) GPIOA_BASE)
// ...
typedef struct
{
    __IO uint32_t CRL;
    __IO uint32_t CRH;
    __IO uint32_t IDR;
    __IO uint32_t ODR;
    __IO uint32_t BSRR;
    __IO uint32_t BRR;
    __IO uint32_t LCKR;
} GPIO_TypeDef;

Структура GPIOA группирует регистры одного порта. Настройка порта производится выставлением определенных битов в этих регистрах. Выставим высокий логический уровень на первой ножке порта A:

GPIOA.ODR = 0x00000001;

По сути мы обращаемся по адресу 0x40000000 + 0x10000 + 0x0800 + 0x4 + 0x4 + 0x4 + 0x4 (0x40010810), куда записывается значение 0x00000001. Эквивалентная запись будет выглядеть так:

*((uint32_t *)0x40010810) = 0x00000001;

Библиотека CMSIS всего лишь вводит синонимы (через макросы) для адресов, чтобы упростить работу разработчика.

Указатель на функцию

Кроме вызова непосредственно функции по указателю, ее можно передавать в качестве аргумента в другую функцию. Рассмотрим такую возможность на примере функции сортировки массива.

void sort(uint32_t* arrray, uint32_t size, uint32_t flag);

Ничего необычного; первый аргумент — это указатель на массив, второй — его размер, а третий, flag, просто указывает функции, как именно мы хотим отсортировать массив: от меньшего к большему или от большего к меньшему. Получилось не очень читаемо. Что нужно записать во flag для сортировки по убыванию? Непонятно.

Можно поступить чуть хитрее и передать в качестве аргумента функцию сравнения, но для этого потребуется немного «магии».

typedef uint32_t (*callback)(uint32_t, uint32_t);

Мы создали тип указателя на функцию, которая принимает два значения uint32_t и возвращает результат в uint32_t. Перепишем функцию сортировки:

// bubble sort algorithm
void sort(uint32_t* array, uint32_t size, callback operator) {
    uint32_t tmp;
    for(uint32_t i = 0; i < size; i++) {
        for(uint32_t j = size - 1; j > i; j--) {
            if (operator(array[j - 1], array[j])) {
                tmp = array[j - 1];
                array[j - 1] = array[j];
                array[j] = tmp;
            }
        }
    }
}

Далее нужно создать две функции, которые будут возвращать результат сравнения.

uint32_t ascending(uint32_t a, uint32_t b) {
    return a > b;
}
​
uint32_t descending(uint32_t a, uint32_t b) {
    return a < b;
}

Для того чтобы отсортировать от меньшего к большему, достаточно вызвать функцию:

// median filter
sort(adc_values, BUFFER_SIZE, descending);
uint32_t mean_value = adc_values[BUFFER_SIZE / 2];

Лямбда-функции в Си

Если вас не смутил заголовок, то, вероятно, вы либо не в курсе, что в Си нет лямбд, либо не знаете, что это такое.

В языке C++ (и многих других) есть так называемые анонимные функции, которые можно вызывать в любом месте в коде, при этом они не имеют идентификатора (имени), и их тело записывается прямо в месте вызова. Вот как мог бы выглядеть предыдущий пример на C++:

std::sort(s.begin(), s.end(), [](int a, int b) {
    return a > b;
    });

В Си такой возможности нет, но можно прибегнуть к использованию макросов и указателей на функцию.

Рассмотрим следующий пример. Допустим, у нас есть функция, которая перед отправкой шифрует данные. При этом клиента может быть два: один принимает данные по SPI, а другой по UART, при этом длина пакета у них разная — 16 и 8 бит соответственно. Другими словами, нам нужно аж три функции: функция шифрования, функция отправки по SPI и функция отправки по UART. Но можно использовать возможности языка.

Во-первых, в GCC допустимо определять функцию внутри другой функции.

void encrypt(uint32_t* data, void (*send_method)(uint32_t*)) {
    // ...
    send_method(data);
}
// ...
​
int main(void) {
    // ...
    void send_spi(uint32_t* data) {
        // ...
    }
​
    enctypt(data, send_spi);
}

Во-вторых, допустимы вложенные выражения.

encrypt(arr, ({
    void send_spi(uint32_t* data) { /* code here */ }
    send_spi;
    }));

Для простоты напишем небольшой макрос и перепишем вызов функции.

Как эта препроцессорная магия работает, я не разобрался; возможность такого использования была позаимствована из статьи «Lambda expressions in C», Guillaume Chereau Blog

#define LAMBDA(c_)              ({ c_ _;})
// ...
encrypt(data, LAMBDA(void _(uint32_t data[]){
    // code
    }));

Модификатор указателя

Для указателей существует специальный модификатор restrict (он появился начиная со стандарта c99), который сообщает компилятору, что области памяти не пересекаются. Причем это никак не контролируется со стороны компилятора: непересечение областей гарантирует программист. В некоторых случаях, обладая такой информацией, компилятор может сгенерировать более оптимальный код.

Рассмотрим следующий пример:

uint32_t *var_a;
uint32_t *var_b;
uint32_t *value;
// ...
*var_a += *value;
*var_b += *value;

Обратите внимание, никто не говорил, что value и var_a не пересекаются. Если они указывают на один и тот же адрес, то содержимое value изменится при записи в переменную по указателю var_a. Чтобы избежать неопределенного поведения, компилятор вынужден два раза считывать значение value.

  1. Загрузить значение value в r1.
  2. Загрузить значение var_a в r2.
  3. Сложить значения value и var_a.
  4. Сохранить результат в var_a.
  5. Загрузить значение value в r1.
  6. Загрузить значение var_b в r2.
  7. Сложить значения value и var_b.
  8. Сохранить результат в var_b.

Если компилятор уверен, что области не пересекаются, то второй раз считывать value и сохранять его в регистр ядра r1 не имеет смысла — значение уже лежит там.


Изменено: