Русский
Русский
English
Статистика
Реклама

Split

Подробная инструкция по изготовлению сплит клавиатуры на основе ATmega32U4. Аналог Iris Jiran своими руками

15.08.2020 14:04:40 | Автор: admin

Моя самоделка


image
Перед изготовленияем своей клавиатуры я наметил следующие цели:
1. Максимально возможный тактильный комфорт.
2. Добиться того, чтобы совершенно не было необходимости смотреть на клавиатуру при работе, чтобы она никоим образом не отвлекала внимание на себя, чтобы даже смотреть на нее было бессмысленно. Именно поэтому на моих колпачках нет надписей.
3. Клавиатура это мой рабочий инструмент, сосредотачиваемся на ее функционале и удобстве. Внешний вид совершенно неважен. Кто не может жить без RGB подсветки и внешней броской красоты нам с вами не по пути, эта статья совсем про другое. В этой статье я вас научу как недорого и просто сделать себе удобный и функциональный инструмент для работы.
4. Удобство перевозки. Сделать компактное и удобное для переноски устройство.

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


Муки выбора раскладки


Итак, поехали. Мы хотим сделать сплит-клавиатуру, гуглим Iris keyboard, выглядит она примерно так:
image
Можно было бы такую и купить, но на ней отсутствуют кнопки Х и Ъ и переучиваться на новое их расположение у меня нет никакого желания. Так клавиатура неплоха, но нам она не подходит. Наш соотечественник придумал удобный вариант jian keyboard, смотрим:
image
Но и он не удобен, получился концепт, годный лишь для работы в текстовых редакторах. На мой взгляд, он тут перестарался, устранив верхний ряд цифровых клавиш. Есть доработанный вариант jiran keyboard:
image
За этой работать будет уже комфортнее, в отличие от предыдущей. Мне при работе в Word клавиши курсора, F1-F12, блок Delete, PgDown и т.д. вполне комфортно нажимать через Fn клавишу. Правой руке намного меньше приходится тянуться к мышке это огромнейший плюс, который начинаешь понимать, только прочувствовав его. На этой основе можно начинать разработку своего варианта. Первый мой прототип выглядел вот так:
image
Могу сразу сказать пять клавиш под большой палец это много. Палец достает с трудом до четвертой кнопки, а до пятой приходится тянуться. Это я тогда пожадничал. И, забегая вперед, скажу очень не хватало курсорных клавиш. На нынешней раскладке у меня под большими пальцами по 4 кнопки, расположенные по дуге. Мне удобно:
image
Переходим по ссылке: keyboard-layout-editor и в вкладке Presets выбираем Ergodox. Кнопки можно удалять, добавлять, поворачивать и двигать по своему усмотрению. Потренируйтесь, там нет ничего сложного.
image
Когда освоитесь, переходите по этой ссылке на мой проект: ErgoRU
Тут вам нужны координаты кнопок по оси Y, точнее разница вертикальных рядов друг относительно друга. Но есть возможно важный именно для вас нюанс у большинства мужчин безымянный палец длиннее указательного, а у женщин немного иначе.
Википедия: Пальцевый индекс
Какое это имеет значение? Возможно, вам придется подкорректировать расположение вертикальных рядов под свои индивидуальные особенности. Мне было проще, необходимости в корректировке не было:
image
Это может показаться вам малозначительным нюансом, но из таких вот маленьких деталей и складывается конструктор под названием Моя идеальная клавиатура.
После многочисленных корректировок у меня получилось вот так:
image
У вас далеко не сразу, но постепенно выработается свой собственный вариант удобной раскладки. Я просто уверен, что эргономика клавиатуры не может быть универсальной, когда пальцы и тип выполняемой работы за клавиатурой у всех сильно отличаются.
Возможно, я приведу пример не самый удачный, но он показателен. Пилотам Формулы1 сиденья и спинки к ним ставят изготовленные по их слепку тела, а не так называемые спортивные, выпускаемые на заводском конвейере. В самом деле, почему?
Почему я реализовал отдельным блоком клавиши курсора помимо тех, которые нажимаются через модификатор МО(2)? Я проектировал клавиатуру универсальной, на которой можно еще и поиграть. Попробуйте поиграть в X3 terran conflict/X3 albion prelude с курсорами через модификатор и вы взвоете. Переназначить кнопки можно, я даже делал четвертый слой именно под игру, но получалось не очень удобно. В этой игре практически вся клавиатура задействована, кто играл тот знает.
Мы можем узнать получившееся количество кнопок:
image

Приступаем к покупкам


И вот теперь, зная количество нужных кнопок, пришло время выбирать и покупать комплектующие. Механические кнопки различаются между собой требуемой силой нажатия, громкостью и тактильными ощущениями. Вот статья про них:
МЕХАНИЧЕСКИЕ ПЕРЕКЛЮЧАТЕЛИ
А после прочтения я настоятельно рекомендую вам прошвырнуться по магазинам, где продаются клавиатуры с механическими переключателями и собственными пальцами ощутить разницу между синими, коричневыми и остальными свитчами. Меня полностью устроили коричневые Gateron, их я и купил. Но только не у наших барыг с наценкой 300%-500% и еще выше. На Алиэкспрессе есть все, нужно лишь хорошо постараться поискать.
Я не призываю покупать нужные нам комплектующие там, где брал я вы можете брать, где вам удобнее. Но ссылками я поделюсь.
Переключатели Gateron на Али, которые купил я:
Gateron
Существуют следующие типы колпачков для кнопок:
Все, что нужно знать при выборе профиля кейкапов
image
Я пробовал разные, но остановился на самых низких DSA, на них мне удобнее всего! Помните, у вас может и скорее всего будет иначе. Помните? Идем в магазин и пробуем, пробуем. Это важный кирпичик удобства вашей клавиатуры. Брал тут, качество хорошее:
PBT DSA
Следующее, что нам нужно, ATmega32U4 5Вольт 16 МГц. Их много модификаций есть:
Atmega32U4
Мне же достаточно будет этого с головой:
Pro Micro ATmega32U4 5V 16 МГц
Диоды. Я не заморачивался с беготней по радиомагазинам, заказал тоже на Али:
100 шт 1N4148 DO-35
Далее идут недорогие и необязательные, но очень важные нюансы нашей будущей клавиатуры.
Смазка для переключателей она уменьшает шумность и улучшает тактильные ощущения при печати в лучшую сторону. Коротко о том, как смазывать свитчи:

Я по наводке приобрел эту смазку, жалеть не пришлось:
Смазка
Резиновые колечки для колпачков.
image
Нужны для уменьшения шума от печати, брал белые 9х4х2.5, нареканий нет:
Резиновые колечки
Кабель Micro USB для подключения платы к компьютеру. Так-то у меня их полно, но нужен был именно угловой, с ним намного удобнее (смотри фото в начале статьи):
Кабель Micro USB

Клавиатуру я сделал разделенной на два блока. Эти блоки нужно было соединить проводами. Поначалу я соединил блоки шлейфом от компьютерного СD привода.
image
Временное решение, для постоянного использования не советую. Лучше сделать съемный соединительный кабель, проще будет в дальнейшем. Хорошо подошел кабель HDMI, у него 19 жил при нужных 12 (5 строк + 7 столбцов). Резать кабель мне очень не хотелось, и я приобрел разъемы для него:
разъем HDMI 19 Pin
Вопрос: можно ли использовать длинный кабель HDMI? Можно, и даже нужно! Просто посмотрите на фото:
image
Клавиатура не сползает с подлокотников. Запястья рук лежат на подставках под ладони. Каждая подставка двумя магнитами держится за свой блок клавиатуры. Провод HDMI ногам не мешает совершенно. Ничего никуда не уползает, получилось просто круто.
Вам, возможно, пригодятся такие переходники:
HDMI кабель-переходник 270 180 90 градусов
image
Вы можете выбрать любой другой способ соединения, какой вам больше подойдет. Смотрите, как выкрутился один из пользователей Реддита, только учтите, что в этом кабеле всего 10 жил:
image
Это я давненько сфоткал. Тот пост уже вряд ли существует, так что только в таком качестве.
Сложим мои расходы, получилось ~38,5$:
image

Покупки будут ползти из Китая ориентировочно месяц. И у нас полно времени, чтобы сделать корпус нашей клавиатуры.

Делаем верхнюю панель и корпус клавиатуры


Возвращаемся к нашему онлайн-конструктору. Надо ввести данные, какой тип свитча мы используем:
image
И следующая картинка:
image
Сейчас нам понадобится вот эта вкладка:
image
Комбинацией Ctrl+A выделяем весь текст и копируем его в буфер обмена Ctrl+C. Открываем ссылку:
Plate & Case Builder
image
И получаем вот такое:
image
Не повезло, изображение расползлось. Не беда, поправим в Photoshop:
image
Отправляем печатать это на принтер. Измеряем линейкой размеры квадратов на бумаге везде должны быть 14 мм. А расстояние между квадратными отверстиями должно быть не менее 5 мм, иначе колпачки будут задевать друг друга. У меня сохранились распечатки промежуточных результатов:
image
Это мы только что распечатали трафарет, с помощью которого будет удобно делать разметку для отверстий под клавиши. Далее вы должны выбрать материал верхней панели вашей будущей клавиатуры: алюминий, стеклотекстолит, оргстекло, акрил, поликарбонат Материалов очень много всяких разных, я же выбрал оргстекло. Используйте материал толщиной не более 2 мм, иначе кнопки не будут держаться на пластине защелки не смогут зацепиться.
image
Только вот прежде, чем приступать работать с вашей будущей панелью, очень вам советую примерить кнопки на куске картона. Так вы получите примерное представление об удобстве расположения кнопок вашей будущей клавиатуры.
image
Когда вы наконец-то решите, что выбрали вариант, который полностью вас устраивает, можно будет приступать к изготовлению верхней пластины клавиатуры. У меня это было вот так:
image
image
Разметку на оргстекло можно наносить как процарапывая чертилкой, так и просто рисуя ручкой ее хватит. Далее я плавил отверстия паяльником, а доводил их до размера напильником. Можно поработать и дремелем. Не забывайте, посылкам ползти из Китая месяц. Торопиться некуда, поэтому я и не носился в поисках лазерной резки.
image

Это верхние пластины первого варианта, в которые вставляются кнопки. Нижние пластины точно такие же по размерам, только без отверстий под свитчи. Осталось только придумать способ крепления верхних пластин к нижним. Я реализовал крепление на стойках, как материнские платы в компьютерных корпусах. В нижних пластинах просверлил отверстия такого диаметра, чтобы в отверстия помещалась гайка М3.
image
И вклеиваем в эти отверстия наши гайки с помощью суперклея.
image
В гайки вкручиваем стойки.
image
Стойки я выбрал по высоте такие, чтобы впритык хватало высоты для свисающих с верхней пластины свитчей.
image
Чем меньше получится высота нашей клавиатуры, тем лучше для наших рук! Высота корпуса моей клавиатуры с резиновыми ножками менее 12мм:
image
Полностью высота составляет 25 мм:
image
В дальнейшем я планирую найти где-нибудь резину/резиновое уплотнение квадратного сечения и приклеить его по периметру нижней пластины, сделав таким образом стенку корпуса клавиатуры. Просто не успел еще.
Вариант 2 до обтягивания пленкой:
image
Далее совсем необязательная вещь я обтянул пленкой верхнюю панель клавиатуры. Но в паре мелочей она все же помогла: бликов от настольной лампы больше нет и теперь не видно светящий красным светодиод на работающей ардуине.
image
Объяснять, как клеить пленку не вижу необходимости. Все что требовалось от этой пленки не отвлекать на себя внимание при работе. И эту задачу она отлично выполняет. Куплена была на Али года четыре назад.
image
Материал, который пошел на подставки под ладони. Как называется, не имею ни малейшего понятия. Вспененное что-то там. Обернул скотчем, а сверху наклеил пленку.
image
Под скотч в двух местах подложил небольшие металлические шайбы, а напротив них к корпусу клавиатуры приклеил выломанные из древнего жесткого диска неодимовые магниты.
image
Теперь подставки под ладони никуда не денутся от клавиатуры магниты очень хорошо держат. И при необходимости убрать подставки совсем не сложно притяжение магнитов перебороть вы точно сможете. Очень советую вам также с магнитами сделать, не пожалеете.

Переходим к изготовлению прошивки


Возвращаемся к онлайн-редактору, нам нужна вот эта вкладка:
image
Комбинацией Ctrl+A выделяем весь текст и копируем его в буфер обмена Ctrl+C. С помощью скопированного текста мы можем увидеть, как примерно будет выглядеть наша будущая клавиатура. Переходим по ссылке:
Keyboard Layout 3D Viewer
image
Можно повращать камеру мышкой и поиграться с зумом. В общем, разберетесь.
Следующий этап, открываем в новой вкладке браузера ссылку онлайн-конструктора нашей прошивки для ардуины:
Keyboard Firmware Builder
Вставляем скопированное, как на скриншоте:
image
Автоматически получится такая корявая и непригодная для дальнейшего использования разводка электрических проводов.
image
Надо помочь алгоритму сработать правильно, сам он справиться не в состоянии. Для этого возвращаемся в онлайн-конструктор и приводим нашу клавиатуру к виду:
image
Заново открываем вкладку Keyboard Firmware Builder и вставляем скопированное:
image
Результат отличный:
image
То, что нужно. Вы должны добиться подобной разводки и у себя. Далее переходим к вкладке PINS, в ней мы укажем, какими контактами ардуины будем пользоваться. Смотрите внимательно, если вы купили такую же плату, как у меня, то у вас в наличии будут только такие контакты как на рисунке ниже:
image
Исходя из того, что мы имеем, выставляем в столбцах ROWS и COLUMNS пины, которые есть в наличии. Ниже на скриншоте пример расстановки:
image
Необязательно расставлять пины в том же порядке, как на картинке выше. Делайте в порядке, удобном вам. Главное, не ошибитесь и не укажите пины, которые отсутствуют на плате ардуины.
Переходим к вкладке KEYMAP, в ней мы зададим поведение кнопок:
image
Частично конструктор присвоил значения кнопкам за нас, доделаем остальное. Тут очень интересно, можно столько всего напридумывать! Начнем с элементарного:
image
И следом жмем:
image
И кнопке присваивается значение 3. Далее аналогично присваиваем значения остальным кнопкам. Вот пример, как сделать правую или левую клавиши Windows:
image

Это все у нас была основная раскладка (слой ноль) по умолчанию, давайте теперь сделаем второй слой.
image
Как пользоваться этой МО(1)? Она работает аналогично клавише SHIFT. Только клавиатурой выдаваться будет то, что мы запрограммируем на слое 1. Начинаем наполнять слой1:
image
Сделаем клавишу DELETE:
image
По умолчанию эта кнопка работает, как BACKSPACE. Но теперь, если мы ее нажмем, удерживая нажатой МО(1), то кнопка будет работать как DELETE. Вот так все просто. Аналогично присваиваем значения остальным кнопкам. Причем необязательно всем. Если мы не присвоили кнопке значение на слое 1, то она будет давать значение слоя ноль (который по умолчанию). Давайте сделаем мультимедийные клавиши управления громкостью.
image
VOLD, VOLU это регулировка громкости. Управление плеером, например Foobar2000: PLAY, STOP, PREV, NEXT. И это все глобальные клавиши, которые будут управлять плеером даже если вы при этом будете работать в WORD либо играть в какую-то игру.
Далее будет волшебство, недоступное обычным клавиатурам. Вспомните, как часто вам приходилось убирать руку с клавиатуры к мышке, чтобы прокрутить (проскроллить) колесиком мышки документ в WORD или страничку в браузере. Часто, наверное? Открываем ссылку:
Mouse Keys
Сейчас мы научим клавиатуру вести себя как компьютерная мышь.
image
image
Смотрите, что я могу сделать одной левой рукой! Я могу двигать курсором мыши (если честно, этим почти не пользуюсь), могу нажимать левую и правую кнопки мыши (периодически клацаю правую), кручу колесо мыши (использую все время, даже привыкать не пришлось!) и управляю плеером Foobar2000 и громкостью. Ладонь при этом ни на миллиметр не смещается в сторону.
image
С этим, я думаю, всем должно быть все понятно. А давайте сделаем, чтобы комбинация Ctrl+Alt+Del нажималась одной кнопкой! Не то чтобы очень надо, но знать, как делается полезно.
image
Следующая операция:
image
Снова модификатор:
image
Следующая операция:
image
Наконец-то уже сама кнопка:
image
Должно получиться вот так:
image
И подобных комбинаций можно придумать множество.

Давайте сделаем слой МО(2):
image
В слое МО(2) я у себя расставил клавиши F1 F12, клавиши курсора и PgDn/ PgUp/ Home/ End. Insert, как и Caps Lock, я счел ненужной клавишей и не стал вводить ее у себя.
image
Таким образом, я могу с помощью этого слоя, вообще никак не перемещая правую ладонь, пользоваться клавишами курсора и блоком PgDn/ PgUp/ Home/ End. Уж поверьте, это удобно! После этого смотреть на обычные стандартные клавиатуры даже не хочется.
Все мы играем в компьютерные игры, одна из моих любимых NFS Underground. Давайте создадим слой кнопок, предназначенный только для этой игры! Естественно, удерживать во время игры все время нажатой кнопку МО(3), чтобы рулить клавишами курсора будет очень неудобно. Вместо него используем модификатор TG(3) он аналогичен принципу работы клавиши Caps Lock. Нажали клавишу TG(3) и отпустили клавиатура остается в режиме работы слоя TG(3). Нажали и отпустили эту клавишу еще раз клавиатура вернется в режим работы слоя по умолчанию. Вот так все просто. Для примера давайте представим себе, что игра не позволяет изменять клавиши, а нам хочется настроить под себя поудобнее.
image
Я сделал себе вот так:
image
Для удобства я замылил на скриншоте неиспользуемые кнопки. На обеих половинках клавиатуры сделал клавиши курсора на случай если рука затечет и захочется дать ей отдохнуть. Не забыл в этом слое реализовать клавишу TG(3) чтобы можно было вернуться из этого слоя в слой по умолчанию. Сделал также в слое 3 клавишу МО(1) для управления мультимедийными кнопками. Ну а остальное в принципе и так понятно. Очевидно, с таким слоем кнопок играть точно будет комфортнее.
Идем далее. Всего слоев можно сделать 15 штук. Мне сложно представить, как их можно все задействовать. И еще тяжелее представить, как их всех запомнить. Я сейчас использую только 4 слоя и пока их достаточно.
Я вам рассказал лишь про малую часть способов кастомизации прошивки. Но того, что рассказал уже достаточно для вас, чтобы начали эффективнее работать с клавиатурой. Хотите нафаршировать свою клавиатуру больше смотрите там:
Коды клавиш
Как сохранить свои наработки, чтобы в следующий раз не начинать все заново:
image
И вот теперь мы можем забрать готовую прошивку с этой вкладки:
image
Теперь нам нужно залить прошивку в нашу ардуину. Проще некуда. Переходим по ссылке:
QMK Toolbox
image
А теперь качаем последнюю версию программы:
image
Инсталлируем программу, на всякий случай перезагружаем компьютер и с потрясением смотрим как вовремя нашей Windows10 приспичило обновиться.
Сейчас вам нужно будет взять в руки паяльник и припаять двумя проводками кнопку к ардуине к контактам GND и RESET.
image
Подключаем кабелем micro usb к компьютеру нашу плату на ней должен загореться светодиод. Запускаем программу QMK Toolbox. Выбираем файл прошивки *.hex, который мы недавно скачали.
image
Жмем на кнопку, которую мы перед этим подпаяли к ардуине, и ждем появления желтой строки на темном фоне программы. Как только она появилась, не тормозим и быстренько нажимаем на кнопку FLASH:
image
Если вы все же протормозили и появилась вторая желтая строка кнопка FLASH не начнет прошивку платы. Начинайте заново: нажимаем припаянную кнопку и т.д.

Переходим к пайке деталей


Пришло время рассказать вам, как правильно спаять электронные компоненты.
Для начала нужно распаять диоды на кнопках. Вам необходимо будет выучить маркировку диодов. Тут все просто: сторона диода, промаркированная нарисованным черным кольцом катод, другая анод. Что это такое и зачем оно нужно вы должны были выучить в школе.
image
Смотрите, что из себя представляет левая часть клавиатуры:
image
А теперь посмотрите на фото ниже:
image
Верхний ряд клавиш ESCAPE,1,2,3,4,5 мы соединили диодами. Диоды мы спаяли друг с дружкой катодами, а сами диоды припаяли к контактам кнопок анодами. Точно так же мы поступаем со строкой кнопок TAB,Q,W,E,R,T. Это второй ряд кнопок сверху. Диоды спаиваем между собой катодами, в общем, точно так же, как и верхний ряд кнопок. Остальные ряды спаиваем диодами точно так же. Следом распаиваем проволочки как на фото ниже.
image
Я специально паял разноцветными проводками от кабеля витой пары, чтобы можно было легко разобраться. Внимательно посмотрите на правый край фото. Я соединил синей проволокой свободные контакты ВЕРТИКАЛЬНОГО РЯДА клавиш: ESCAPE,TAB,ALT,CTRL,TG(3). С остальными вертикальными рядами поступаем совершенно точно также.
Когда вы все это спаяете, можно будет переходить к подключению ардуины. Вспоминаем эту картинку:
image
И сравниваем ее с этой картинкой:
image

Проволоку, соединяющую вертикальный ряд клавиш ESCAPE,TAB,ALT,CTRL,TG(3) соединяем проводом к контакту ардуины С6. Проволоку, соединяющую вертикальный ряд клавиш 1,Q,A,Z, TG(4) соединяем проводом к контакту ардуины D7. Проволоку, соединяющую вертикальный ряд клавиш 2,W,S,X,LShift соединяем проводом к контакту ардуины E6. Далее действуем по аналогии. Переходим к строкам. Спаянные между собой катоды диодов строки ESCAPE,1,2,3,4,5 подключаем к контакту ардуины D3. Спаянные между собой катоды диодов строки TAB,Q,W,E,R,T подключаем к контакту ардуины D2. Катоды диодов строки Alt,A,S,D,F,G подключаем к контакту D1. И далее аналогично. Правая половина клавиатуры готовится аналогично левой.
image
Если б не клавиши курсора, фото ниже не было бы таким запутанным:
image
Но мы то уже знаем, что на самом деле все просто строки кнопок соединяем диодами, а столбцы кнопок проволочками по схеме перед этим фото. И как подключить этот блок клавиатуры, вы тоже уже должны понимать сами.
Спаянные между собой катоды диодов строки UP,6,7,8,9,0,] подключаем к контакту ардуины D3. Спаянные между собой катоды диодов строки DOWN,Y,U,I,O,P,[ подключаем к контакту ардуины D2. И так далее. Проволоку, соединяющую вертикальный ряд клавиш UP,DOWN,LEFT,RIGHT соединяем проводом к контакту ардуины B2. Проволоку, соединяющую вертикальный ряд клавиш 6,Y,H,N, МО(1) соединяем проводом к контакту ардуины B3. И снова далее по аналогии.
Когда соберете конструкцию, запустите на компьютере Блокнот и проверьте как работают кнопки. У меня ни с первого, ни со второго раза нормально работать клавиатура не хотела я перепутывал провода, подпаивая их не на свои контакты ардуины. Было также, что я по неаккуратности коротил их между собой. Исправить несложно берете мультиметр и прозваниваете провода, какой куда ведет. Просто откройте эти картинки на экране монитора перед собой и проверяйте куда на самом деле ведут провода.
image

Вот и все. Основываясь на этой статье, вы без проблем сможете изготовить для себя клавиатуру, заточенную под вас и удобную лично для вас. Такую клавиатуру, которую вы не сможете купить ни за какие деньги нигде и никогда.
P.S. Если вы решитесь сделать свою клавиатуру, не стесняйтесь делитесь своими наработками! Мне всегда очень интересно посмотреть на новые идеи. Возможно, благодаря вам я смогу добавить к своей клавиатуре что-то новое и классное.
Подробнее..

Разбиваем строку на подстроки по разделяющим символам своими руками

19.03.2021 16:09:40 | Автор: admin

Введение

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

Вообще говоря, сама задача разбиения строк на подстроки, каждая из которых отделена в исходной строке определённым символом, является довольно распространённой. Очень часто необходимо извлечь из строки слова, разделённые пробелами. Конечно, в стандартной библиотеке языка Си уже есть функция strtok (заголовочный файл <string.h>), но она имеет свои побочные эффекты, перечисленные ниже.

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

  2. Так как функция модифицирует строку, то при передачи ей строчного литерала, будет получено SEGV, поскольку память для таких литеральных строк выделяется в сегменте кода, доступного только для чтения.

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

  4. Она не учитывает экранирование символов разделителей.

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

  1. Не менять оригинальную строку, в которой ищутся лексемы.

  2. Для каждой найденной лексемы создавать новую строку.

  3. Сохранять свою текущую позицию, а именно - указатель на подстроку, которая ещё не разбиралась.

  4. Иметь однородную последовательность вызовов.

  5. Иметь возможность экранировать символы разделители, при сложных лексемах.

Основные шаги при разделении строк.

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

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

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

Разработка функции.

Приступим к разработке. Для начала определим заголовочный файл "str_utils.h", содержащий все прототипы необходимых функций. Реализации функций положим в файл "str_utils.c".

Начнём с функции нахождения символов в строке. Библиотечная функция strchr могла бы решить эту задачу. Но проблема в том, что она не допускает в качестве аргумента строки, в которой надо искать символ, значения NULL. При попытке компилировать с флагами -Wall -Werror, файл с таким аргументом не скомпилируется. Хотя, такую ситуацию можно было бы обработать, вернув NULL. Поэтому был определён свой вариант данной функции с именем contains_symbol. Её прототип выглядит следующим образом:

size_t contains_symbol(char *src, char symbol);

Её реализация определена следующим образом (файл "str_utils.c"):

size_t contains_symbol(char *src, char symbol){  size_t pos = 1;  if(symbols == NULL)    return -1;  while(*symbols != '\0'){    if(*symbols++ == symbol)      return pos;    pos++;  }  return 0;}

Данная функция возвращает позицию символа в строке, увеличенную на единицу. Она не учитывает нулевой символ. Если символ не был найден, функция вернёт 0, либо -1, если ей передали NULL. Её удобно использовать в цикле while, при проверке текущего символа строки на его наличие в другой строке.

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

void *alloc_mem(size_t nbytes);void *calloc_mem(size_t nelems, size_t elem_size);#define alloc_str(x) ((char *) alloc_mem(x + 1))

Соответствующие функции реализованы в отдельном файле "mem.c":

#include <string.h>#include <stdlib.h>void *alloc_mem(size_t nbytes){  char *buf = (char *)malloc(nbytes);  if(buf != NULL){    memset(buf, '\0', nbytes);    return buf;  }  exit(-1);}void *calloc_mem(size_t nelems, size_t elem_size){  void *buf = calloc(nelems, elem_size);  if(buf != NULL){    return buf;  }  exit(-1);}

Они выделяют блок памяти в куче, содержащий указанное количество байт, а также дополнительно его обнуляют (функция alloc_mem).

Функция обрезки разделителей строки trim_separators выглядит следующим образом:

/* trims symbols from separators at src string *//* returns new trimmed string */char *trim_separators(char *src, char *separators);
char *trim_separators(char *src, char *separators){  if(src == NULL || separators == NULL)    return NULL;  char *sp = src;  while(contains_symbol(separators, *sp)) sp++;    /* if it contains only symbols from separators => NULL */  if(sp - s == strlen(s)) return NULL;    char *sp2 = s + strlen(s) - 1; /* last char at src */  while(contains_symbol(separators, *sp2)) sp2--;    /* if it contains only symbols from separators => NULL */  if(sp2 < s) return NULL;    size_t sz = 0;  if(sp2 - sp == 0 && *sp == '\0') return NULL; /* zero byte is not a character */  else if(sp2 - sp == 0){    sz = 1;  }  else{    sz = (sp2 - sp) + 1;  }  char *res = alloc_mem(sz);  memcpy(res, sp, sz);/* copy all chars except last zero byte */  return res;}

В начале мы проверяем на NULL аргументы функции. Если они нулевые, то возвращаем NULL.

Далее, через указатель sp, проходим строку слева направо, пока мы встречаем символы из строки separators. Если мы прошли всю строку, значит она целиком и полностью состоит из сепараторов, следовательно надо удалить все символы, или же просто вернуть NULL.

char *sp = src;while(contains_symbol(separators, *sp)) sp++;  /* if it contains only symbols from separators => NULL */if(sp - s == strlen(s)) return NULL;

Аналогично, далее через указатель sp2, проходим строку справа налево, проверяя, находится ли текущий символ в массиве separators. Если это не так, то мы прерываем цикл, а указатели будут содержать ссылку на первые символы, не являющимися разделителями. Если мы опять прошли всю строку, значит снова придётся удалять всю строку, следовательно, возвращаем NULL.

char *sp2 = s + strlen(s) - 1; /* last char at src */while(contains_symbol(separators, *sp2)) sp2--;  /* if it contains only symbols from separators => NULL */if(sp2 < s) return NULL;

Наконец, вычисляем длину строки. Если указатели ссылаются на одно и то же место, то в строке был лишь один символ, не являющийся разделителем, а потому размер результата будет равным 1 байту (один лишний байт для нулевого символа учтён в макросе alloc_str). Если же этот единственный символ является нулевым (маркером конца), то возвращаем NULL. Иначе берём разницу между адресами указателями, прибавляем к ней единицу, и получаем длину новой строки. Затем мы просто выделяем память для новой строки и копируем в неё строку, начинающуюся с указателя sp.

Теперь, объединим работу выше написанных функции, в единую функцию get_token().

Код функции get_token дан ниже:

char *get_token(char *src, char *delims, char **next){  if(src == NULL || delims == NULL)    return NULL;  char *delims_p = delims;  /* the end of lexem (points to symbol that follows right after lexem */  char *src_p = trim_separators(src, delims);  /* the begining of the lexem */  char *lex_begin = src_p;  if(src_p == NULL){    *next = NULL;    return NULL;  }    /* flag that indicates reaching of delimeter */  int flag = 0;  while(*src_p != '\0'){    flag = 0;    while(*delims_p != '\0'){      if(*delims_p == *src_p){        flag = 1;        break;      }      delims_p++;    }    if(flag == 1)      break;    delims_p = delims;    src_p++;  }    /* now src_p points to the symbol right after lexem */  /* compute lexem size and reset pointers (from trimmed to the original src) */  char *offset;  size_t tok_size;  offset = (src + strspn(src, delims));  tok_size = (src_p - lex_begin);  free(lex_begin);  lex_begin = offset;  src_p = offset + tok_size;  if(*src_p == '\0')    *next = NULL;  else    *next = src_p;    /* result token */  char *res = alloc_str(tok_size);  memcpy(res, lex_begin, tok_size);  return res;}

В ней используется функция обрезки trim_separators(). Функция обрезки возвращает новую строку, и далее сканирование ведётся по ней. В цикле лишь проверяется, не равен ли текущий символ какому-либо символу разделителю из массива символов delims, и если равен, то выйти из цикла. Указатель src_p проходит по сканируемой строке. После цикла он будет указывать на символ, следующий за лексемой (конец лексемы). А начало лексемы сохраняется в указателе lex_begin, который изначально указывает на начало обрезанной, сканируемой строки. После обнаружения границ лексемы, вычисляется её размер (её число символом), а затем сканируемая строка удаляется из динамической кучи. Затем указатели переустанавливаются на позиции в оригинальной строке (первый аргумент функции get_token()), а часть строки, которая ещё не была разобрана, присваивается в качестве содержимого двойному указателю next. Обратите внимание, что next является ссылкой на другой указатель (в данном случае, на указатель строки). Двойной указатель позволяет менять значение переменной типа char *, записывая новый адрес в next. Для первого вызова данной функции, next должен хранить адрес переменной указателя, которая указывает на строку и хранит адрес первой ячейки строки. Однако, при работе с двойным указателем возможна серьёзная и незаметная ошибка, если в качестве начального значения next передать адрес переменной, которая непосредственно указывает на строку, а не адрес переменной копии, которая содержит копию адреса строки. В следующем разделе подробно описана данная ситуация, и показан пример работы данной функции.

Пример работы get_token().

Ниже дан простой рабочий пример функции get_token(). Оригинальная строка с лексемами хранится в указателе test, копия адреса строки (копия переменной test) хранится в переменной copytest. Указатель tok хранит текущую распознанную лексему, а next - сканируемую часть строки. Данная программа разделяет строку test по пробелу и символу табуляции на подстроки, и выводит их. Также она выводит саму строку test до и после работы функции. Как можно убедиться по выводу, оригинальная строка не меняется.

#include <stdio.h>#include <stdlib.h>#include <string.h>#include "mem.h"#include "str_utils.h"int main(int argc, char **argv){  char *test = "  They have  a    cat.\n \0";  char *copytest = test;  char **next = &copytest; /* has side effect on copytest */  char *tok = NULL;    printf("src:%s\n", test);  printf("copytest:%s\n", copytest);  while(*next != NULL){    tok = get_token(*next, " \t\0", next);    if(tok == NULL)      break;    printf("%s\n", tok);    free(tok);  }  printf("src after:%s\n", test);  printf("copytest after:%s\n", copytest);  return 0;}  

Вывод данной программы:

src:  They have  a    cat.copytest:  They have  a    cat.Theyhaveacat.src after:  They have  a    cat.copytest:(null)

Обратите внимание, что в цикле есть дополнительная проверка на NULL указателя tok. Дело в том, что при получении последнего слова в строке (а именно "cat.\n"), указатель next будет указывать на подстроку, состоящую лишь из одних пробелов (плюс нулевой символ). Функция trim_separators()для таких строк возвращает NULL, так как по логике придётся урезать все символы в строке. В итоге get_token() также вернёт NULL, поскольку уже ничего не осталось для сканирования. Поэтому переменная tok сохранит значение NULL, на последнем шаге.

Теперь снова по поводу двойного указателя next. Как вы могли заметить, в вышеприведённом коде ему передаётся адрес переменной copytest, а не переменной test. Дело в том, что мы можем нечаянно затереть значение переменной test (именно переменной, а не самой строки). Для примера, изменим код следующим образом. Передадим адрес test в указатель next. В итоге мы получим следующий вывод.

src:  They have  a    cat.copytest:  They have  a    cat.Theyhaveacat.src after:(null)copytest:  They have  a    cat.

Как видите, сама строка не меняется, но изменилось значение переменной test. Теперь она содержит NULL, поскольку на последнем шаге, функция присваивает ей соответствующее значение. Отсюда следует, что операции вида:

*next = addr;*next = NULL;

с двойными указателями (указатель на указатель), тройными, и какой-либо сколь угодно длинной цепочкой указателей создают побочный эффект.

Модификация функции get_token(). Экранирование разделителей.

Функция get_token() умеет возвращать новые подстроки (токены) из исходной строки, не меняя её. Однако она совершенно не умеет их экранировать, в случае, когда лексемы представляют собой более сложные объекты. Например, а что если лексема содержит символ, который мы выбрали в качестве разделителя?

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

1233,"John Cenna","Male",4.22,"2004, 2005, 2006",11234,"John Doe","Male",4.24,"2001, 2004, 2007",01235,"Maria Laws","Female",4.23,"2003, 2006, 2008",1

Данные значения формируют следующую таблицу:

Id

Name

Gender

Coefficient

Years

IsActive

1233

John Cenna

Male

4.22

2004, 2005, 2006

yes

1234

John Doe

Male

4.24

2001, 2004, 2007

no

1235

Maira Laws

Female

4.23

2003, 2006, 2008

yes

Заметим, что в данном случае, значения отделяются друг от друга запятой, причём сам разделитель (запятая) не учитывается, когда он находится в кавычках. Это значит, что сам разделитель дополнительно экранирован (escaped) двойными кавычками.

Для таких особых случаев, была определена новая функция get_token_escaped(), которая в качестве дополнительного параметра принимает массив (строку) символов, экранирующих разделители. Символы разделители не должны пересекаться с данным массивом. Т.е. один и тот же символ должен быть либо управляющим, либо разделяющим но не одним и тем же одновременно. Если же это так, то функция будет возвращать NULL. Для контроля начала и конца экранируемой последовательности, была заведена отдельная переменная neflag. Переменная neflag указывает, не является ли текущий символ частью экранируемой последовательности (neflag => not escaped flag). Она равна 0, когда символ должен быть экранируем, и 1, когда символ не экранируется. В цикле сканирования, сначала текущий символ ищется среди экранирующих (escaped). Если он найден и он равен предыдущему управляющему символу, то устанавливаем соответствующий флаг neflag равным 0, так как была найдена пара - начала и конец экранируемой последовательности. Если же это другой экранирующий символ, не равный предыдущему, то если мы уже находимся в экранируемой последовательности, то ничего не надо делать (продолжаем поиск, в надежде, что мы отыщем нужную пару (пред. символ)). Иначе, если мы нашли его впервые, то запоминаем его в переменной esym, и сбрасываем флаг neflag в 0. Символ разделитель будет учтён, если он был обнаружен (flag == 1) и он не является частью экранируемой последовательности (neflag == 1).

Ниже дан код данной процедуры.

char *get_token_escaped(char *src, char *delims, char *escaped, char **next){  if(src == NULL || delims == NULL || escaped == NULL)    return NULL;  char *delims_p = delims;  char *escaped_p = escaped;  /* the end of lexem (points to symbol that follows right after lexem */  char *src_p = trim_separators(src, delims);  /* the begining of the lexem */  char *lex_begin = src_p;  if(src_p == NULL){    *next = NULL;    return NULL;  }    /* check that (delims INTERSECTION escaped) IS NULL. */  /* IF NOT => return NULL */  int err = 0;  while(*delims_p != '\0'){    while(*escaped_p != '\0' && (err = (*escaped_p == *delims_p) ) != 1) escaped_p++;    escaped_p = escaped;    if(err){      return NULL;    }    delims_p++;  }  delims_p = delims;    /* flag that indicates reaching of delimeter */  int flag = 0;  /* flag that indicates that we are not in escaped sequence */  int neflag = 1;  /* previously saved escape character (i.e. the begining of the escaped seq.) */  char *esym = (char)0;  while(*src_p != '\0'){    flag = 0;    while(*escaped_p != '\0'){      if(*src_p == *escaped_p && *src_p == esym){        neflag = 1;        esym = (char)0;        break;      }      else if(*src_p == *escaped_p && neflag){        neflag = 0;        esym = *escaped_p;        break;      }      escaped_p++;    }    while(*delims_p != '\0'){      if(*delims_p == *src_p){        flag = 1;        break;      }      delims_p++;    }    if(flag && neflag)      break;    delims_p = delims;    escaped_p = escaped;    src_p++;  }    /* now src_p points to the symbol right after lexem */  /* compute lexem size and reset pointers (from trimmed to the original src) */  char *offset;  size_t tok_size;  offset = (src + strspn(src, delims));  tok_size = (src_p - lex_begin);  free(lex_begin);  lex_begin = offset;  src_p = offset + tok_size;  if(*src_p == '\0')    *next = NULL;  else    *next = src_p;    /* result token */  char *res = alloc_str(tok_size);  memcpy(res, lex_begin, tok_size);  return res;}

Пример её использования дан ниже:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include "mem.h"#include "str_utils.h"int main(int argc, char **argv){  char *test = "  They have  \"cats dogs mice  \"\n \0";  char *copytest = test;  char **next = &copytest; /* has side effect on copytest */  char *tok = NULL;    printf("src:%s\n", test);  printf("copytest:%s\n", copytest);  while(*next != NULL){    tok = get_token_escaped(*next, " \t\0", "\"\0", next);    if(tok == NULL)      break;    printf("%s\n", tok);    free(tok);  }  printf("src after:%s\n", test);  printf("copytest after:%s\n", copytest);  return 0;}

Вывод:

src:  They have  "cats dogs mice  "copytest:  They have  "cats dogs mice  "Theyhave"cats dogs mice  "src after:  They have  "cats dogs mice  "copytest after:(null)

Заключение

Разработанная функция get_token() позволяет извлекать лексемы в отдельные новые строки. Она не меняет исходной строки, и имеет одинаковую последовательность вызовов. Из недостатков, она использует двойной указатель для сохранения текущей позиции сканера. Чтобы не затирать значение переменной, адрес которой содержит двойной указатель next, необходимо передавать адрес другой переменной, являющейся копией исходной (см. код выше). Функция также не умет экранировать символы разделители. Эту работу делает другая функция - get_token_escaped().

Функцию get_token_escaped() можно использовать при работе с CSV файлами. Однако ей должны передаваться непересекающиеся множества символов разделителей, и экранирующих символов. Иначе будет неоднозначность между ними. Функция не умеет пока анализировать такие неоднозначности, поэтому в таких случаях она вернёт NULL. Кроме того, она не допускает вложенных экранируемых последовательностей. Т.е. если был встречен экранируемый символ, всё что следует за ним, включительно до его клона (такого же символа, но в следующей позиции), считается как одна экранируемая последовательность.

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru