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

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

Я проанализировал кодовые таблицы различных терминалов и среду разработки программ МК CodeVisionAVR. Последняя использует кодировку CP1251. То есть, все строковые функции при написании и компиляции программы подчиняются именно этой кодовой таблице. Затем я проанализировал мой любимый HyperTerminal с настройками по умолчанию. Там ситуация поинтереснее. При передаче символов используется всё та же CP1251, но при приёме символы выводится в кодировке CP866. Однако если в настройках поменять шрифт с «Terminal» на другой, то вывод на экран становится также CP1251. В виртуальном терминале Proteus ситуация почти аналогична. Изменение шрифта на другой решает проблему приёма кириллицы. А со шрифтом по умолчанию кириллица просто игнорируется. Отсюда следует, что ничего дополнительно разрабатывать не нужно. Достаточно поменять шрифт в терминале и можно приступать к реализации ввода-вывода кириллицы. Это действительно так, но у меня есть ещё один терминал на смартфоне, который работает с МК через Bluetooth или TCP/IP. Об этом я неоднократно писал в прошлых статьях. В таком терминале нет настроек шрифта или кодировки, и, как я выяснил опытным путём, он работает в кодировке UTF-8, как на приём, так и на передачу. Именно эта кодировка и послужила мне камнем преткновения. Но я всё равно запланировал реализацию преобразования кодировок по различным комбинациям, которые перечислены ниже.

  • Преобразование №0 (по умолчанию, без преобразования): CP1251 -> CP1251; CP1251 <- CP1251.

  • Преобразование №1 (для настроенного по умолчанию HyperTerminal): CP1251 -> CP1251; CP866 <- CP1251.

  • Преобразование №2: CP866 -> CP1251; CP866 <- CP1251.

  • Преобразование №3: UTF-8 -> CP1251; UTF-8 <- CP1251.

  • Преобразование №4: CP1251 -> CP1251; UTF-8 <- CP1251.

Для начала я составил сводную таблицу в Excel с кодами значений кириллических символов для различных кодовых таблиц. На рисунках ниже - скриншоты начала и конца таблицы соответственно.

Начало таблицы
Начало таблицы
Конец таблицы
Конец таблицы

Кириллические символы – заглавные и строчные буквы русского алфавита, включая букву «ё», а также несколько других распространённых букв из других алфавитов для коллекции. В столбце «H» можно видеть разность между значениями кодов одних и тех же символов в кодировках CP1251 и CP866. Для всех заглавных и половины строчных букв, исключая «Ё/ё», разность в значении составляет 64. Для всех остальных, начиная с «р» и заканчивая «я» – 16. Для дополнительных букв, включая «Ё/ё», – отдельный случай. Таким образом, при реализации алгоритма преобразования кодировок CP1251 и CP866 нужно рассматривать два диапазона и особые случаи.

Теперь по поводу UTF-8. Про эту кодировку я слышал ещё со школьных времён и знал о ней весьма приблизительно. Считал, что символы этой таблицы кодируются не одним, а двумя байтами. Однако только сейчас я узнал, что это и вовсе не таблица, а способ кодирования символов из таблицы юникода с помощью формата переменной длины. Для решения моей задачи можно было абстрагироваться от этого правила, представив, что для русского алфавита UTF-8 представляет собой таблицу, где каждому символу соответствует два байта. Но я решил разобраться в алгоритме кодирования и декодирования UTF-8 более детально. Я не буду здесь писать подробности, как это работает, об этом уже написали другие. Отмечу лишь, что символы от 0 до 127, куда включена основная латиница, кодируются в UTF-8 одним байтом, а с 128 до 2048 – двумя. Представленные значения берутся из таблицы юникода, но первые 128 символов в ней совпадают с таблицей CP1251. А вот значения кодов символов кириллицы лежат в диапазоне, образно говоря, от 1000 до 1200. Поэтому для их представления в UTF-8 требуется два байта. На рисунке в моей таблице Excel это столбец «F» (коды символов из юникода). Разность значений юникода и CP1251 для одних и тех же символов русского алфавита, исключая «Ё/ё» оказалась постоянной и составила 848 (столбец «G»). Для дополнительных букв, включая «Ё/ё», – также отдельный случай. Таким образом, в алгоритме преобразования кодировок UTF-8 и CP866 нужно рассматривать один диапазон и особые случаи, но предварительно – реализовать кодирование и декодирование между UTF-8 и юникодом.

Можно приступить к реализации. Вернёмся к моему простому проекту из статьи про управляющие символы и последовательности. Функция getchar() принимает байт из UART, точнее, из кольцевого буфера. Вместо этой функции реализована функция getcharn(code), где code – номер кодировки, согласно вышеперечисленным комбинациям преобразований. Кстати, его значение можно менять специальной командой терминала, выбирая требуемую кодировку. Обработку данной команды также нужно не забыть придумать и прописать. В новой функции getcharn через switch-case перечислены все случаи по комбинациям преобразований. Самая сложная секция – «case 3», в которой помимо преобразования реализовано декодирование из UTF-8 в юникод с применением битовых масок. Там есть одна немаловажная особенность. Если алгоритм видит, что символ UTF-8 состоит более чем из одного байта, то последующие байты функцией getchar забираются сразу же. Но если вдруг передача данных в МК прервётся «посреди символа», программа зависнет в ожидании очередного байта. Поэтому есть идея реализовать этот момент каким-нибудь другим способом.

unsigned char getcharn(unsigned char c){
    unsigned char d,t;
    unsigned int u=0;
    t=getchar();
    d=t;
    switch(c){
        case 3: //UTF-8 -> CP1251
            if(!(t&0x80)){ //Латиница
                octet=0;
                d=t;
            }
            if((t&0xE0)==0xC0){
                octet=1;
                u=t&0x1F;
            }
            if((t&0xF0)==0xE0){
                octet=2;
                u=t&0x0F;
            }
            if((t&0xF8)==0xF0){
                octet=3;
                u=t&0x07;
            }
            while(octet){
                t=getchar();
                if((t&0xC0)==0x80){
                    octet-=1;
                    u=(u<<6)|(t&0x3F);
                }
            }
            if(u>=1040&&u<=1103){ //А...я
                d=(unsigned char)(u-848);
            }
            if(u==1025){ //Ё
                d=168;
            }
            if(u==1105){ //ё
                d=184;
            }
        break;
        case 2: //CP866 -> CP1251
            if(t<128){ //Латиница
                d=t;
            }
            if(t>=128&&t<=175){ //А...п
                d=t+64;
            }
            if(t>=224&&t<=239){ //р...я
                d=t+16;
            }
            if(t==240){ //Ё
                d=168;
            }
            if(t==241){ //ё
                d=184;
            }
        break;
        case 4: //CP1251 -> CP1251
        case 1: //CP1251 -> CP1251
        default: //CP1251 -> CP1251
            d=t;
        break;
    }
    return d;
}

Теперь про реализацию преобразования кодировок на случай передачи информации. Здесь тоже напишу несколько слов касательно нетрадиционного подхода к передаче символов и текста по UART. Изначально я привык это делать с помощью функции printf. В CodeVisionAVR это вполне работает, и очень удобно. А если нужно передать один байт, то я писал printf(“%c”,byte). Но когда возникла необходимость пользоваться вторым UART’ом в Atmega128, то функция printf перестала работать. И я перешёл на функции putchar1 и putstr. Первая функция с единичкой в конце отличается от встроенной putchar – в ней я добавил строчку while(!(UCSRA&(1<<UDRE))), чтобы исключить работу функции «в фоне». Если быть точнее, функцию putchar1 построил мне конфигуратор CodeWizardAVR при указании второго UART (UART1). Но потом я её дополнил и стал ей пользоваться даже при работе с UART0. А функция putstr – моя функция, выводящая строку. Но если требуется вывести строку с применением спецификаторов, что я пользуюсь композицией функций sprintf(str,…..) и putstr(str). Так вот, для реализации преобразования кодировок я ввёл функцию putcharn(chr, code). То есть, в аргументе указывается не только код выводимого на терминал символа, но и номер комбинации преобразования кодировок. Отдельно реализована функция utf8, работающая в качестве ограниченного кодера из юникода в UTF-8 на случай применяемых мной символов. А в функции putcharn реализованы остальные простейшие преобразования также с применением switch-case. Там всё понятно без комментариев.

void utf8(unsigned int h){
    if(h>=0&&h<128){ //Для латиницы
        putchar1(h);
    }
    if(h>=1000&&h<2048){ //Для кириллицы
        putchar1(0xC0|(h>>6));
        putchar1(0x80|(h&0x3F));
    }
}
void putcharn(unsigned char d, unsigned char c){
    unsigned char t;
    unsigned int u;
    t=0;
    switch(c){
        case 1: //CP866 <- CP1251
        case 2: //CP866 <- CP1251
            if(d<128){ //Латиница
                t=d;
            }
            if(d>=192&&d<=239){ //А...п
                t=d-64;
            }
            if(d>=240&&d<=255){ //р...я
                t=d-16;
            }
            if(d==168){ //Ё
                t=240;
            }
            if(d==184){ //ё
                t=241;
            }
            putchar1(t);
        break;
        case 3: //UTF-8 <- CP1251
        case 4: //UTF-8 <- CP1251
            if(d<128){ //Латиница
                u=(unsigned int)d;
            }
            if(d>=192&&d<=255){ //А...я
                u=d+848;
            }
            if(d==168){ //Ё
                u=1025;
            }
            if(d==184){ //ё
                u=1105;
            }
            utf8(u);
        break;
        default: //CP1251 <- CP1251
            putchar1(d);
        break;
    }
}

На этом можно закончить эту короткую статью.

Комментарии (0)