Comrade Bulkin Memory Blog

Мои заметки и статьи по тематике программирования МК, а также системного администрирования


Project maintained by firebull Hosted on GitHub Pages — Theme by mattgraham

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

Я долго не мог придумать, что же такое взять в качестве примера из того, что у меня самого не реализовано. И с удивлением обнаружил, что у меня нет библиотеки для классического текстового LCD на Hitachi HD44780. Это 1-но, 2-х или 4-х строчные дисплеи до 20 символов на строку. Те самые, которые все так любят втыкать во все свои DIY. Полазил по просторам и с ещё большим удивлением обнаружил, что все реализации для шины I2C основаны на дурацкой классической библиотеке Arduino LiquidCrystal_I2C. Ну, думаю, сам Бог велел!

Документация

Начнём с главного: чтения документации.

Как работает дисплей

Дисплей основан на старинном чипе от HITACHI HD44780. У него нет последовательного интерфейса, как, например, у ST7920. Тем не менее, он прост до безобразия.

Открываем даташит раздел “Interfacing to the MPU” и видим примерную диаграмму, как устроен обмен данными. Смотрим, смотрим и видим фигу. Но всё-таки что-то почерпнуть можно. Скрин

Базовый его режим 8-ми битный. Т.е. мы можем передавать ему 1 байт за раз. Для чего у него есть восемь ног DB0-DB7. Ещё у него используются 3 ноги:

  • E: выдаём строб (импульс), который сообщает дисплею, что на ногах DB0-DB7 выставлены нужные данные, мол, давай, считывай
  • RS: Сообщаем дисплею, что мы хотим передать или считать, команду или конфигурацию
  • R/W: Сообщаем дисплею, пишем мы данные или считываем

На схеме показывается 4-битный режим. Это когда мы используем 4 ноги DB4-DB7 вместо восьми и передаём два раза по 4 бита. Режим полезен там, где жалко отдавать лишние ноги у МК. Или для нашего расширителя портов на PCF8574.

Пытливый ум заметит, что сначала мы передаём старшие 4 бита, потом младшие. Также обратит внимание, что для передачи данных на ноге R/W должен быть 0, а для чтения 1.

Итак, как же выглядит передача данных в 8-битном режиме:

  • Для передачи команды дисплею, на ноге RS мы выставляем 0. Если надо передать символ, выставляем 1;
  • Если мы передаем команду или данные, то выставляем 0 на ноге R/W;
  • На ногах DB0-DB7, мы выставляем значения побитово того, что хотим передать;
  • Выдаём строб (импульс) на ноге E;
  • Документация рекомендует после строба считывать готовность дисплея к приёму следующей команды.

Как же выглядит передача данных в 4-битном режиме:

  • Для передачи команды дисплею, на ноге RS мы выставляем 0. Если надо передать символ, выставляем 1;
  • Если мы передаем команду или данные, то выставляем 0 на ноге R/W;
  • На ногах D4-D7 дисплея, мы выставляем значения старших 4-х бит, что хотим передать;
  • Выдаём строб (импульс) на ноге E;
  • На ногах D4-D7 дисплея, мы выставляем значения младших 4-х бит, что хотим передать;
  • Выдаём строб (импульс) на ноге E;
  • Документация рекомендует после двух стробов считывать готовность дисплея к приёму следующей команды.

Я тут накидал диаграмку, как передаются данные в 4-х битном режиме. Передаём два байта 0xA6 и 0xE9.

Скрин

Обратите внимание, нельзя вот просто так взять и щёлкнуть стробом. Нужно помнить, что ширина строба и пауза между ними должны соответствовать паспортным данным. Идём в даташит и ищем что-то похожее на delay, timeout, execution time и т.д. Обязательно даются такие данные. Находим табличку “Table 6: Instructions” и видим, что на исполнение команды требуется от 37мкс до 41мкс. На возврат курсора в начало экрана требуется 1.52мс. Также при хаотичном листании документа в поисках информации, какая же должна быть пауза, находим в диаграмме “Figure 24: 4-Bit Interface” это:

When BF is not checked, the waiting time between instructions is longer than the execution instuction time. (See Table 6.)

Т.е. если мы не проверяем флаг занятости дисплея (почему объясню позже), то пауза должна быть больше, чем время исполнения инструкции. Т.о. я указал на диаграмме ширину строба 50мкс, интервал между парными стробами тоже в 50мкс, а интервал между данными 60мкс, для гарантии (китайские микрухи такие китайские).

Сами символы хранятся в таблицах, которые бывают Японскими или Кириллическими. На Али кириллицу хрен купишь, поэтому мы можем только загрузить в дисплей 8 собственных символов. Полностью это алфавит не покроет, но хоть что-то.

Как с ними работать, будем смотреть позже. Сейчас нас волнует подключение и протокол.

Подключение дисплея к шине I2C

Но нам вот жалко отдавать 7 ног МК (в 4-битном режиме) на дисплей. И кто-то взял и придумал копеешный модуль, который цепляет дисплей к I2C и сохраняет старый протокол.

Основан он на расширителе портов PCF8574. Вещь простая до безобразия. У него есть 8 ног, на которых мы можем выставлять 0 или 1. По I2C мы тупо шлём один байт на него, где каждый бит соответствует ноге. Либо тупо считываем такой же байт с текущим состоянием этих самых ножек.

Так вот модуль подключен по аналогичной схеме (я реализовывал это у себя на плате года два назад): Скрин

Пытливый ум, глядя на эту схему, задастся вопросом: А как же строб выдавать? Да ещё тайминги соблюдать. Да и вообще, как дрыгать ножками RS да R/W, чтоб не мешать данным и не сводить с ума дисплей? А вот тут и начинается самое интересное.

Ход мыслей такой. Давайте сначала заглянем в документацию PCF8574 и поищем там диаграмму по обмену данными. Находим прекрасную картинку: Скрин

Внимательно смотрим и видим, что состояние на ногах меняется сразу по окончании приёма байта от МК. Т.е. нам нужно передать данные и выставить ногу P2 в высокий уровень чтобы включить строб. Потом передать данные и выставить P2 уже в ноль, т.е. строб мы выключаем. А для этого нам надо разобраться, что такое шина I2C и с чем её едят.

Шина I2C

Откровенно говоря, не люблю я её. Использую только там, где нет альтернативы. Скорость небольшая, ёмкость линии ограничена 400пФ, в результате длина линии очень маленькая. К тому же сама суть протокола имеет существенный недостаток, об этом позже. Для каждого готового устройства приходится вручную подбирать номиналы подтягивающих резисторов. В этом плане SPI гораздо удобнее и круче, хоть и требует минимум 3-х ног. Ладно, к сути.

I2C - это асимметричная последовательная шина, использует две двунаправленные линии. Т.е. данные передаются последовательно в одном направлении. Обе линии подтянуты к питанию. Шина типа “Ведущий-Ведомый”. Теоретически возможно ситуация, когда хоть все устройства могут быть как ведущим, так и ведомым. На практике реализовать это почти невозможно.

Для понимания работы, надо сначала запомнить правила:

  • Данные на линии SDA могут меняться только при низком уровне на линии SCL
  • Пока на линии SCL высокий уровень, на линии SDA данные не меняются
  • Утрируя, есть три состояния: СТАРТ, СТОП и передача данных.
  • Формировать сигналы СТАРТ и СТОП может только ведущий, даже в случае приёма им данных от ведомого
  • Адрес ведомого устройства состоит из 7-ми бит.

Сигнал СТАРТ - это перевод линии SDA в низкий уровень при высоком уровне линии SCL.

Сигнал СТОП - перевод линии SDA в высокий уровень также при высоком уровне SCL.

Т.о. для начала передачи данных ведомогу, ведущий формирует сигнал СТАРТ. Все ведомые устройства на линии начинают слушать. Затем ведущий выстреливает адрес ведомого, с которым он хочет поговорить и сажает SDA на ноль. Адрес этот, как видно по картинке выше, занимает старшие 7 бит, а последний бит задаёт читаем мы данные или пересылаем. Если устройство на линии есть, оно удержит линию SDA в низком уровне, это значит, что оно готово общаться. Тоже самое и по окончании приёма данных. По окончании передачи ведущий формирует сигнал СТОП.

Вот тут и кроется главная проблема шины I2C. После передачи данных, если ведомый занят, он может продолжать удерживать линию SDA. Ведомый также может удерживать и SCL, если он не успевает обрабатывать данные, т.е. ведомый может снижать скорость передачи данных. По стандарту, устройства должны управлять линиями по схеме Open Drain. И даже если какое-то устройство монопольно займёт линию, другое сможет её сбросить. Теоретически. На практике же, если, например, ведомый подвис и держит линию, а мы поднимаем её на ведущем, оживить ведомого порой можно только reset’ом. Там вообще такие бывают дичайшие комбинации, что однажды даже пришлось прокидывать отдельную линию RESET для ведомых устройств и периодически их дергать.

Итак. Более менее и в общих чертах мы разобрались с I2C. На wiki есть неплохая статья, да и вообще погуглите, шина непростая, я дал лишь общую информацию для понимания вопроса.

Приступаем к написанию библиотеки

Вот и настал момент, когда мы почти готовы написать первые строки кода. Давайте сначала посмотрим, как устроены другие библиотеки. Мы же ленивые и надеемся обойтись малой кровью.

Откроем классическую Arduino LiquidCrystal_I2C. Просто бегло пройдём по ней глазками. Не знаю, как у вас, у меня сразу глаз цепляется за несколько вещей:

  • Используются аппаратные задержки
  • Куча однотипных функций
  • Нет никаких оптимизаций по экономии потребления памяти
  • Нет контроля ошибок
  • Нет вменяемых комментариев

Если мы просто пороемся на GitHub в поисках библиотек для STM32, почти все они будут на основе этой же LiquidCrystal_I2C. С теми же недостатками. Я не буду глубоко туда влезать, я просто сделаю всё по-своему.

Итак, составим требования к нашей библиотеке:

  • Никаких аппаратных задержек
  • Использовать DMA для передачи данных
  • Минимум функций, максимально выносить всё в #define
  • Максимально экономим память
  • Каждое обращение к дисплею должно контролироваться

Создаём проект

Для начала надо создать проект. Я уже написал инструкцию, как правильно настроить STM32CubeMX у себя в блоге, не буду повторяться тут. Полностью проект с уроком доступен в моем репо на GitHUB.

Отмечу только, что урок написан для отладочной платы на STM32F303VC. У меня сейчас нет под рукой STM32F103C8, так что всё проверял на STM32F3DISCOVERY. Но адаптировать под любую другую плату можно без особых проблем.

Дальше, конечно, мы можете взять готовую библиотеку, я её выложил на GitHub. Я вкратце напишу, что я делал.

Создадим два файла:

Inc/lcd_hd44780_i2c.h
Src/lcd_hd44780_i2c.c

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

Скрин

Отлично! Всё написано по шагам, с таймингами и даже биты указаны! Но мы любопытные и хотим сразу знать, что же битики значат, чтобы сразу заполнить заголовочный файл #define’ами. Вспоминаем про “Table 6: Instructions”. Там прям идеально, с комментариями, расписаны все биты команд.

Открываем наш заголовочный файл и предварительно накидываем:

#define LCD_BIT_RS                 ((uint8_t)0x01U)
#define LCD_BIT_RW                 ((uint8_t)0x02U)
#define LCD_BIT_E                  ((uint8_t)0x04U)
#define LCD_BIT_BACKIGHT_ON        ((uint8_t)0x08U)
#define LCD_BIT_BACKIGHT_OFF       ((uint8_t)0x00U)

#define LCD_MODE_4BITS             ((uint8_t)0x02U)
#define LCD_BIT_1LINE              ((uint8_t)0x00U)
#define LCD_BIT_2LINE              ((uint8_t)0x08U)
#define LCD_BIT_4LINE              LCD_BIT_2LINE
#define LCD_BIT_5x8DOTS            ((uint8_t)0x00U)
#define LCD_BIT_5x10DOTS           ((uint8_t)0x04U)
#define LCD_BIT_SETCGRAMADDR       ((uint8_t)0x40U)
#define LCD_BIT_SETDDRAMADDR       ((uint8_t)0x80U)

#define LCD_BIT_DISPLAY_CONTROL    ((uint8_t)0x08U)
#define LCD_BIT_DISPLAY_ON         ((uint8_t)0x04U)
#define LCD_BIT_CURSOR_ON          ((uint8_t)0x02U)
#define LCD_BIT_CURSOR_OFF         ((uint8_t)0x00U)
#define LCD_BIT_BLINK_ON           ((uint8_t)0x01U)
#define LCD_BIT_BLINK_OFF          ((uint8_t)0x00U)

#define LCD_BIT_DISP_CLEAR         ((uint8_t)0x01U)
#define LCD_BIT_CURSOR_HOME        ((uint8_t)0x02U)

#define LCD_BIT_ENTRY_MODE         ((uint8_t)0x04U)
#define LCD_BIT_CURSOR_DIR_RIGHT   ((uint8_t)0x02U)
#define LCD_BIT_CURSOR_DIR_LEFT    ((uint8_t)0x00U)
#define LCD_BIT_DISPLAY_SHIFT      ((uint8_t)0x01U)

Это та самая нудная часть работы, о которой я говорил. Внимательно смотрим в табличку, двоичный код переводим в HEX. Поясню на примере:

Инструкция Display on/off control требует всегда выставленного бита DB3. Открываем калькулятор, вводим двоичное 1000 и получаем 0x08 HEX.

В самой инструкции есть три команды:

  • Display on/off
  • Cursor on/off
  • Blinking of cursor position character

Калькулятором высчитываем их HEX и будем их потом суммировать с LCD_BIT_DISPLAY_CONTROL.

Биты RS, RW, E и Backlight относятся к PCF8574, так что не забываем прописать и их.

Позже аналогичным способом напишем и остальные #define.

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

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

typedef struct {
    I2C_HandleTypeDef * hi2c;  // I2C Struct
    uint8_t lines;             // Lines of the display
    uint8_t columns;           // Columns
    uint8_t address;           // I2C address shifted left by 1
    uint8_t backlight;         // Backlight
    uint8_t modeBits;          // Display on/off control bits
    uint8_t entryBits;         // Entry mode set bits
} LCDParams;

Обратите внимание. В этом struct мы храним не саму структуру для I2C, а лишь указатель. Т.о. мы не дублируем данные и всегда под рукой их состояние.

Судя по алгоритму инициализации, первые этапы уникальны и можно реализовать их тупо отправляя данные через базовые функции HAL. Их мы реализуем в функции lcdInit().

Пора бы уже отправить какие-то данные на дисплей. Хорошим тоном будет сделать локальную низкоуровневую функцию, которая будет подготавливать данные для отправки и саму отправку. А мы лишь будем в неё закидывать состояние управляющих битов и байт данных.

А вот и реализация в готовой библиотеке.

/**
 * @brief  Local function to send data to display
 * @param  rsRwBits State of RS and R/W bits
 * @param  data     Pointer to byte to send
 * @return          true if success
 */
static bool lcdWriteByte(uint8_t rsRwBits, uint8_t * data) {

    /* Higher 4 bits*/
    lcdCommandBuffer[0] = rsRwBits | LCD_BIT_E | lcdParams.backlight | (*data & 0xF0);  // Send data and set strobe
    lcdCommandBuffer[1] = lcdCommandBuffer[0];                                          // Strobe turned on
    lcdCommandBuffer[2] = rsRwBits | lcdParams.backlight | (*data & 0xF0);              // Turning strobe off

    /* Lower 4 bits*/
    lcdCommandBuffer[3] = rsRwBits | LCD_BIT_E | lcdParams.backlight | ((*data << 4) & 0xF0);  // Send data and set strobe
    lcdCommandBuffer[4] = lcdCommandBuffer[3];                                                 // Strobe turned on
    lcdCommandBuffer[5] = rsRwBits | lcdParams.backlight | ((*data << 4) & 0xF0);              // Turning strobe off


    if (HAL_I2C_Master_Transmit_DMA(lcdParams.hi2c, lcdParams.address, (uint8_t*)lcdCommandBuffer, 6) != HAL_OK) {
        return false;
    }

    while (HAL_I2C_GetState(lcdParams.hi2c) != HAL_I2C_STATE_READY) {
        vTaskDelay(1);
    }

    return true;
}

Для того, чтобы выдавать строб, мы дважды шлём первую партию 4-х бит. Третьим байтом шлём младшие 4-бит и закрываем строб.

И вот, что получается на деле: Скрин

При таком раскладе и скорости шины I2C в 100кбит, ширина строба ~180мкс, пауза между стробами ~90мкс, и пауза между парными стробами ~530мкс. По идее, и я так думал предварительно, можно не удлинять строб на два байта, обойтись одним. Но на деле оказалось, что 90мкс мало для ширины строба, но достаточно для паузы между стробами. Похоже, что кварц в дисплее работает на более низкой частоте, чем положено по даташиту. Как говорил - Китай такой Китай =( А может мой дисплей дурит.

Также можно сократить длинную паузу раза в два, для этого есть два способа:

  • Использовать прерывания и циклом фигачить побайтово прямо в регистр. Но это постоянные прерывания с обработчиками, на больших данных будут блокировки. А я этого ой как не люблю. Я предпочитаю отправить данные через DMA и забыть о них. И начать заниматься другими делами, пусть МК сам разруливает.
  • Либо создать большущий буфер на отправку, для 20 символьного дисплея это будет порядка 120 байт. Надо будет просто подготовить данные в буфере и отправить одним выстрелом в DMA. Но я решил экономить память.

Но нас интересует вопрос, я так ругал Ардуиновскую библиотеку, а есть ли выигрыш? А вот смотрите, что показывает LiquidCrystal_I2C: Скрин

Комментарии излишни. Но ведь я учу вас критическому мышлению, не так ли? Что если оптимизировать код библиотеки Ардуино? Да! И я уверен, получится значительно улучшить параметры передачи. Делайте, думаю стотыщпятьсот людей вам скажут спасибо. Ведь я к чему это всё говорю. К тому, что в этом и есть беда Ардуино - такой вот код, где никто не думает об оптимизациях. Причём ведь делает вид, что всё согласно даташиту. Например, зачем нужны задержки между передачами в 50мкс, если в реальности между стробами 500мкс??

У пытливого ума опять же возникает вопрос, а есть выигрыш на большой передаче? А вот смотрите, сверху STM32, а снизу LiquidCrystal_I2C, данные одинаковые, процедура инициализации тоже: Скрин Скрин

Итог: STM32 83мс, LiquidCrystal_I2C 122мс. Повторю, если использовать прерывания вместо чистого DMA, можно получить ещё больший выигрыш, думаю вполне реально сократить это время до 60мс. Но надо ли? С таким дисплеем и его откликом это уже за гранью добра и зла =)

Что ещё интересного в библиотеке

Я написал одну единственную функцию, которая занимается командами. Это функция lcdCommand().

Она занимается как установкой параметров, так и снятием. В качестве входных параметров, у неё команда и флаг - снять или выставить команду. Оба параметра - это нумерованные списки LCDCommands и LCDParamsActions.

Обратите внимание, никаких if/else. Всё сделано на Switch/Case. Несмотря на то, что их аж три штуки и приходится как минимум дважды проверять команду, работает это невероятно быстро. И причина тому - Бинарное дерево, в которое компилятор транслирует наш код. По сути, там всего два узла, так что поиск происходит за несколько тактов.

Конечно, вы можете использовать и запись типа if (command == LCD_DISPLAY), это также будет откомпилировано в бинарное дерево, но такой код читается хуже.

В результате мы получили возможность через #define определить прототипы функций, с коротким написанием и удобным чтением:

/* Function defines */
#define lcdBacklightOn()           lcdBacklight(LCD_BIT_BACKIGHT_ON)
#define lcdBacklightOff()          lcdBacklight(LCD_BIT_BACKIGHT_OFF)
#define lcdAutoscrollOn()          lcdCommand(LCD_DISPLAY_SHIFT, LCD_PARAM_SET)
#define lcdAutoscrollOff()         lcdCommand(LCD_DISPLAY_SHIFT, LCD_PARAM_UNSET)
#define lcdDisplayClear()          lcdCommand(LCD_CLEAR, LCD_PARAM_SET)
#define lcdDisplayOn()             lcdCommand(LCD_DISPLAY, LCD_PARAM_SET)
#define lcdDisplayOff()            lcdCommand(LCD_DISPLAY, LCD_PARAM_UNSET)
#define lcdCursorOn()              lcdCommand(LCD_CURSOR, LCD_PARAM_SET)
#define lcdCursorOff()             lcdCommand(LCD_CURSOR, LCD_PARAM_UNSET)
#define lcdBlinkOn()               lcdCommand(LCD_CURSOR_BLINK, LCD_PARAM_SET)
#define lcdBlinkOff()              lcdCommand(LCD_CURSOR_BLINK, LCD_PARAM_UNSET)
#define lcdCursorDirToRight()      lcdCommand(LCD_CURSOR_DIR_RIGHT, LCD_PARAM_SET)
#define lcdCursorDirToLeft()       lcdCommand(LCD_CURSOR_DIR_LEFT, LCD_PARAM_SET)
#define lcdCursorHome()            lcdCommand(LCD_CURSOR_HOME, LCD_PARAM_SET)

А вообще, совет. Там, где у вас чётко обозначенные варианты, использовать switch/case, там, где необходимо сравнивать величины, использовать if/else. И не стесняйтесь нумерованных списков enum - они занимают очень мало памяти. Компилятор сам подбирает тип, но всё также, как с обычными целочисленными переменными, чем больше список, тем больше разрядность.

Почему не проверяем готовность дисплея, как в даташите

А потому, мои дорогие, что в случае с I2C это лишено смысла. Посмотрите на реальную передачу. На один только запрос уходит минимум 1 байт плюс ещё байт на адрес. Итого 180мкс. Для проверки готовности мы сначала должны выставить R/W в 1, потом еще щелкать стробами и внутри 1-го строба проверять бит BF на ноге DB7. Посчитали? Это при том, что по документации занят дисплей от 37мкс до 1,52мс. Проще просто использовать трюк с I2C.

Что можно придумать с русскими символами

У нас есть только возможность загрузить своих 8 символов. Я с этим сталкивался и, скажу, это нелегкий выбор =) Для этого в дисплее есть доступный EPROM на 8 ячеек. Каждая в каждую ячейку можно записать символ из 8 строк по 5 точек в каждой. Соответственно, это массив из 8 байт, где младшие 5 бит и есть наши точки. На самом деле, последняя строка - это курсор, так что, если уж соответсвовать стандартам, на символ можно использовать 5х7 точек. Вот схема из даташита (Example of Correspondence between EPROM Address Data and Character Pattern (5 × 8 Dots)):

Скрин

Например, символ Д в HEX будет такой:

uint8_t symD[8]   = { 0x07, 0x09, 0x09, 0x09, 0x09, 0x1F, 0x11 }; // Д

Соответственно загружаем его в CGRAM функцией:

lcdLoadCustomChar(0, &symD);

и выводим функцией:

lcdPrintChar(0);

Ну а как просто вывести текст?

Это элементарно. Нужно просто выставить курсор и отправить код символа в дисплей. Он сдвинет курсор на следующую позицию автоматически, а мы следом шлём следующий символ. И т.д.

Код символа в C/С++ определяется, если взять его в одиночные кавычки, например, ‘B’. Либо просто перебором берём из строки &data[i].

/**
 * @brief  Print string from cursor position
 * @param  data   Pointer to string
 * @param  length Number of symbols to print
 * @return        true if success
 */
bool lcdPrintStr(uint8_t * data, uint8_t length) {
    for (uint8_t i = 0; i < length; ++i) {
        if (lcdWriteByte(LCD_BIT_RS, &data[i]) == false) {
            return false;
        }
    }

    return true;
}

В готовом виде это:

lcdSetCursorPosition(3, 0);
lcdPrintStr((uint8_t*)"Hello, World!", 13);

lcdSetCursorPosition(3, 1);
lcdPrintStr((uint8_t*)"Hello, GitHub!", 14);

lcdSetCursorPosition(1, 2);
lcdPrintStr((uint8_t*)"LCD Display example", 19);

lcdSetCursorPosition(2, 3);
lcdPrintStr((uint8_t*)"by Comrade Bulkin", 17);

Обратите внимание. Мы отправляем в функцию не строку, а указатель на массив uint8_t. Т.е. мы, во-первых, создаём строку в памяти, во-вторых, преобразуем строку в unsigned int, в-третьих, отправляем указатель на неё. Это, конечно, вариант для примера. В боевых устройствах использовать такую запись плохой тон. Т.к. во-первых, мы используем динамическое выделение памяти, что само по себе в условиях крайне её ограниченности не айс. Лучше стараться заранее выделить память под некоторые переменные. А во-вторых, приходится вручную пересчитывать размер строки. Так что хорошим тоном будет примерно так:

lcdInit(&hi2c2, (uint8_t)0x27, (uint8_t)4, (uint8_t)20);

/* Выведем предопределённую строку */
static const char helloWorld[] = "Hello, world!";
lcdSetCursorPosition(3, 0);
lcdPrintStr((uint8_t*)helloWorld, strlen(helloWorld));

/* Выведем строку по формату */

// Буффер, куда будем записывать генерируемый текст
// Размер буффера - макс. количество символов в строке + 1 на терминальный символ \0
char buffer[21];
    
/**
* В отличие от sprintf, snprintf() в конце строки добавляет символ конца строки \0
* и можно использовать функцию strlen() для подсчета длины строки.
*/
snprintf(buffer, (size_t)21, "Hello, %s", "GitHub!");  // Будет "Hello, GitHub!"

lcdSetCursorPosition(3, 1);
lcdPrintStr((uint8_t*)buffer, strlen(buffer));

/* Ну и выведем просто две строки текста "в лоб" */
lcdSetCursorPosition(1, 2);
lcdPrintStr((uint8_t*)"LCD Display example", 19);

lcdSetCursorPosition(2, 3);
lcdPrintStr((uint8_t*)"by Comrade Bulkin", 17);

Немного о комментариях в коде

Призываю не слушать тех, кто заявляет, что очевидный код не нуждается в комментировании. В этом есть здравое зерно, правда есть несколько суровых НО.

Конечно, глупо комментировать каждую строчку. Но хорошим тоном, по моему опыту, являются:

  • Перед каждой функцией писать стандартный заголовок с тегами @brief и @note. В них стоит описать что это за функция и как она работает. Там же дать описания переменных и что она возвращает. Во многих современных редакторах есть плагин типа Docblockr. Просто перед функцией пишете /** и плагин сам создаёт отформатированный заголовок, вам нужно только дописать ручками несколько строк.
  • Давать отсылки на переменные из других файлов и документацию
  • Если алгоритмов для реализации несколько, напишите, почему выбрали конкретный. Сильно упрости общение с другими в будущем.
  • Добавляйте комменты для выделения этапов и всяких неочевидных вещей

Я сейчас дописываю документацию к библиотеке, читать её можно будет тут.

Напоследок

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

Я постарался заострить внимание на ключевых моментах. Параллельно показать какие-то банальные фишки, которые многие боятся использовать. Например, указатели.

Вам же стоит открыть готовую библиотеку и посмотреть на её устройство собственными глазками. Она элементарная, проблем с чтением кода быть не должно. Но есть моменты, которые стоит соблюдать. Все их зацепить я не могу. Спрашивайте в комментах, постараюсь отвечать подробно.

Оригинал статьи опубликован на Pikabu