В одной из прошлых статей я писал про разработку, точнее, про расширение терминального интерфейса микроконтроллера 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;
}
}
На этом можно закончить эту короткую статью.
LAutour
По оформлению кейсов с циферками: что мешало циферки спрятать в константы (enum или define)?
R3EQ Автор
для наглядности кода можно и спрятать. но результат на практике будет такой же.
LAutour
Это у меня из практики долгого изучения чужого кода с цифрами в case вместо констант, когда долго пытался найти закономерность их выбора, а в итоге оказалось эти цифры были выбраны "от балды".