Авторизация
Регистрация

Напомнить пароль

Магнитометр на базе HMC5883L

Сейчас на разных площадках продаются маленькие и дешевые платы модуля магнетометра, построенные на микросхемах HMC5883L или QMC5883L. Подключив такую плату к Arduino, можно за считанные часы сделать свой магнитометр. Но я хочу показать, сколько времени и труда требуется, чтобы магнитометр оформить в более-менее законченный прибор.



Зачем нужен магнетометр?

Не буду лукавить – магнитометр не является сколько-нибудь необходимым прибором в хозяйстве. Чаще всего он вообще не нужен. Но в некоторых случаях он может оказаться полезным. Сейчас снова возрос интерес к аналоговому аудио. В любом богатом интерьере обязательно должен быть виниловый проигрыватель, а в очень богатом – катушечный магнитофон. При обслуживании аппаратуры магнитной записи есть такая важная процедура, как размагничивание головок и других частей лентопротяжного механизма (ЛПМ). Делая разного рода доработки, желательно всегда проверять, нет ли опасных полей рассеяния от двигателей, электромагнитов и т.д.

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

Парадокс, но посторонние магнитные поля приносят гораздо больший вред в режиме записи, чем в режиме воспроизведения. Потому что картина меняется, когда лента попадает в переменное магнитное поле стирания или подмагничивания. Тогда она становится чувствительной даже к очень слабым магнитным полям, фиксируя их на себе. А даже немного намагниченная постоянным полем лента заметно сильнее шумит. Это так называемый структурный шум. Дело в том, что концентрация ферромагнитных частиц в магнитном слое ленты не постоянна. Если намагнитить реальную ленту постоянным магнитным полем, то остаточная намагниченность будет выше в тех местах, где концентрация магнитных частиц выше. Т.е. намагниченность не будет постоянной, она будет хаотически меняться, что при воспроизведении даст дополнительный шум.

Кроме повышения уровня шума, возрастают и искажения. Характеристика намагничивания у ленты довольно нелинейная, но она симметричная относительно нуля. В результате почти не образуется четных гармоник. Искажения магнитофонов приято измерять по уровню 3-й гармоники. Но если накладывается внешнее постоянное поле, кривая намагничивания смещается вверх или вниз и перестает быть симметричной. Это вызывает появление четных гармоник. Ниже показана анимированная картинка спектра шумов канала записи-воспроизведения катушечного магнитофона «Электроника-004»: один спектр снят при включенном электромагните прижимного ролика, второй – при выключенном (ролик прижимался вручную). Видно, что поле рассеяния электромагнита заметно повышает уровень НЧ-шума, а также резко увеличивает уровень 2-й гармоники.

Влияние поля рассеяния электромагнита


Датчик магнитного поля

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

Магнитное поле можно измерить разными способами, начиная с очень простых в виде иголки на ниточке или стрелки компаса. Переменное магнитное поле можно измерить с помощью любой катушки, используя явление электромагнитной индукции. Давно известны феррозондовые датчики. Они являются активными и позволяют измерять очень слабые постоянные магнитные поля. Используемый в них эффект по сути тот же, что и паразитный эффект от внешнего постоянного поля в магнитофонах – растет уровень четных гармоник, а это можно измерить. Позже появились датчики на основе эффекта Холла и магниторезистивные датчики разных типов. Есть и более сложные датчики магнитного поля, например, на основе ядерного магнитного резонанса.

Для удобства использования нужен какой-то готовый малогабаритный датчик, выпускаемый серийно. Интересуют в основном малые поля, порядка магнитного поля Земли (это примерно 0.5 Гс, или 0.05 мТл). Как раз с такой чувствительностью выпускаются магниторезистивные датчики, предназначенные для построения компасов. Они отличаются очень маленькими габаритами, низким потреблением энергии, простотой применения и низкой стоимостью. Был выбран один из таких датчиков – HMC5883L.

Этот датчик основан на так называемой анизотропной магниторезистивности (AMR, Anisotropic MagnetoResistance). Такой эффект присущ тонким пленкам из ферромагнитных сплавов, например, пермаллоя. Магниторезистивный эффект заключается в том, что под воздействием внешнего магнитного поля сопротивление пленки меняется на 2 – 3%. Тонкая пленка обладает анизотропией: ее свойства неодинаковы в разных направлениях. При изготовлении используют сильное магнитное поле, которое ориентирует домены пленки и задает ось легкого намагничивания, расположенную вдоль длинной стороны магниторезистора.

Сопротивление пленки зависит от угла между вектором намагниченности и направлением тока. Оно будет максимальным, когда направление тока совпадает с направлением этого вектора. Собственная намагниченность направлена вдоль магниторезистора, если ток протекает тоже вдоль, то сопротивление будет максимально. Магниторезистор наиболее чувствителен к внешнему полю, вектор которого лежит в плоскости пленки и направлен перпендикулярно ее длинной стороне. Такое поле складывается с собственным полем, в результате суммарный вектор поворачивается на некоторый угол, что вызывает уменьшение сопротивления. Мы находимся на вершине кривой, внешнее поле любого направления будет лишь уменьшать сопротивление. Причем данный участок характеристики обладает малым наклоном и большой нелинейностью, что очень плохо. Чтобы зависимость сопротивления от внешнего поля была линейной, и чтобы была зависимость от направления внешнего поля, надо работать на склоне характеристики, линейный участок которой расположен симметрично относительно угла 45 градусов.


Но как заставить ток течь под углом 45 градусов? Для этого на пленку под таким углом наносят полоски алюминия. Поскольку он имеет гораздо меньшее удельное сопротивление, чем пермаллой, данные полоски фактически закорачивают участки пленки под собой и разбивают магниторезистор на множество последовательно соединенных участков. В каждом из этих участков ток течет по кратчайшему пути от одной алюминиевой полоски к другой, т.е. под 45 градусов относительно оси пленки. Такую полосатую структуру называют «barber pole».


Может показаться, что это название имеет что-то общее с электрическими полюсами. Но нет, «pole» здесь в значении «столб», а «barber» – парикмахер. Наверняка у входа в барбер-шоп, куда вы ходите подровнять бороду, поболтать о моде и выпить смузи, видели странный трехцветный цилиндр с нанесенными по спирали цветными полосами. Это и есть тот самый «парикмахерский столб», или «barber pole». Разработчики увидели что-то знакомое в топологии AMR-магниторезистора, отсюда и появилось такое название. По легенде, расцветка этого столба уходит в дикие времена.

Тот самый «barber pole»


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

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

В процессе эксплуатации под действием внешнего магнитного поля ориентация магнитных доменов в пленке может нарушится, что нарушит работу датчика. Поэтому ориентацию надо периодически восстанавливать. Для этого датчик окружают катушкой, которая называется «Set/Reset Coil». В катушку подают короткие импульсы тока, в результате домены в пленке ориентируются вдоль оси легкого намагничивания.


Направление намагничивания пленки можно поменять на противоположное, тогда и чувствительность к внешнему полю получится противоположная. Это используют для компенсации смещения и его температурного дрейфа, а также для снижения уровня фликкер-шума. Измерения проводят по очереди с прямой и обратной намагниченностью, подавая перед измерением на катушку сначала импульс «Set», потом «Reset». Затем берут разность выходных напряжений моста. При этом компенсируется не только смещение и дрейф самого моста, но и усилителя.


Рядом с датчиком часто располагают еще одну катушку, которая называется «Offset Coil». Использовать ее можно для разных целей: для компенсации остаточного смещения, для компенсации искажений внешнего поля (вызванных, например, присутствием рядом ферромагнетиков), для самотестирования датчика (что имеется в HMC5883L), а также для смещения характеристики датчика в нужную точку. В последнем случае возможна работа датчика внутри замкнутой петли обратной связи, когда сумма внешнего поля и поля этой катушки всегда поддерживается постоянной. Тогда датчик будет всегда работать в одной точке характеристики, что снимает проблему нелинейности.


Чувствительность датчиков на основе AMR-магниторезисторов примерно в 20 раз выше, чем датчиков на основе эффекта Холла. Кроме AMR существуют еще GMR-магниторезисторы (Giant MagnetoResistance), они используются, например, в головках жестких дисков. Еще бывают TMR-магниторезисторы (Tunnel MagnetoResistance), состоящие из нескольких слоев тонких пленок. Их чувствительность еще выше, чем у AMR. Но для измерения полей порядка магнитного поля Земли, лучше всего подходят AMR-сенсоры.


HMC5883L является модулем, состоящим из нескольких чипов. В его состав входит трехосевой AMR-сенсор семейства HMC118X. Три отдельных измерительных моста расположены ортогонально, чтобы обеспечить измерение компонент магнитного поля вдоль осей X, Y и Z. Второй специализированный чип (ASIC) обеспечивает усиление сигналов с сенсора, их оцифровку с помощью 12-разрядного АЦП, а также управление катушками Set/Reset и Offset. Чип имеет интерфейс I2C для связи с микроконтроллером. Модуль HMC5883L предназначен для поверхностного монтажа и имеет миниатюрный корпус LCC размером 3.0 x 3.0 x 0.9 мм.


У модуля предусмотрены 2 раздельных питания: VDDIO для интерфейса I2C и VDD для внутренней схемы. Эти напряжения могут быть равны, тогда достаточно одного источника питания (например, 3.3 В).

Измеренные значения представляются в виде 16-разрядных чисел со знаком, которые могут лежать в диапазоне от -2048 до 2047. Если произошла перегрузка АЦП или переполнение при математических расчетах результата, то считывается значение -4096. Модуль имеет 3 идентификационных регистра, из которых считываются ASCII символы «H43».

Сделанные на основе этого модуля платы продаются на Aliexpress дешевле 2$. Микросхема HMC5883L от Honeywell имеет маркировку L883. Бывают такие же платы, сделанные на микросхеме QMC5883L от QST Corporation с маркировкой Dx 5883. По поводу этой микросхемы ничего сказать не могу, использовал только L883. Напрямую микросхемы не совместимы, они имеют разные I2C-адреса и разные адреса внутренних регистров. QMC5883L является усовершенствованной версией HMC5883L, более подробно о различиях можно почитать по ссылке.


Готовая плата хоть и не очень крупная, но она мало похожа на датчик магнитометра, который обычно представляет собой тонкий щуп. Поэтому я решил сделать свою плату и перепаять на нее микросхему с китайской платы. Стремился сделать щуп как можно миниатюрней, для этого взял полоску текстолита шириной около 5 мм и толщиной 1 мм. Для микросхемы сделал в плате окно. Несколько конденсаторов обвязки должны быть размещены в непосредственной близости от микросхемы, потому что по ним протекает значительный импульсный ток (до 1 А) катушки Set/Reset. Использовал SMD-конденсаторы размера 0603, для них в плате выфрезеровал углубления. Соединить все это печатными проводниками не получилось, поэтому для монтажа использовал тонкий обмоточный провод в эмалевой изоляции. Сборку платы датчика производил под микроскопом, что оказалось довольно трудным делом.


В результате получилась практически плоская конструкция, над платой ничего не выступает, кроме паек. Плату поместил внутрь термоусадки, получился плоский щуп сечением 5.3 х 1.6 мм.


Осталось сделать для щупа какую-то ручку. В качестве основы я решил выбрать что-то из старых пишущих принадлежностей. Старые ручки и фломастеры я не выбрасываю, а храню в отдельной коробке. Из них потом получается много всего полезного. В данном случае лучше всего подошел корпус от ErichKrause Liquid Ink Roller ER-50. Взял только колпачок и часть корпуса из черного пластика. Немного доработал – удалил крепление, сделал прорезь для платы датчика, внутрь вклеил колечко из части какой-то другой ручки. Для провода сделал отверстие, куда вставил эластичный кабельный выпуск.

Минимально для подключения датчика требуются 4 провода: VCC, GND, SDA, SCL. Наиболее компактным 4-кантактным разъемом является 3.5 мм разъем, который применяется для телефонных гарнитур. Он как раз имеет 4 контакта. Поначалу решил использовать целиком провод от гарнитуры с таким разъемом. Провод состоит из многожильных проводников в лаковой изоляции. Но при подключении через такой провод датчик перестал работать. Оказалась виновата емкость, которая достигает 460 пФ при длине провода чуть больше 1 м! Более-менее тонкий и гибкий 4-жильный провод в ПВХ изоляции удалось найти у какой-то старой компьютерной мыши, его емкость оказалась намного ниже, датчик нормально заработал на скорости 400 кбит/с. Правда, разъем теперь пришлось подпаивать самому, а разборные разъемы более громоздки, чем литые разъемы гарнитур.


Корпус устройства

Обычно я делаю стационарные приборы в настольном исполнении. Так как носить мне их некуда, да и самому ходить особо некуда. Но данный прибор слишком маленький по «начинке», отводить под него стационарный корпус, который будет наполовину пустым, не хотелось. Поэтому решил сделать портативный прибор. Перебрав имеющиеся в продаже корпуса, остановился на хорошо знакомом Kradex Z32. Быстрая прикидка показала, что в этот корпус всё умещается.




Дисплей

Графические дисплеи в своих устройствах я применял и раньше, но все они были монохромными. Маленькие цветные TFT стали доступными довольно давно, но я их в своих конструкциях не применял (хотя применял на работе). В любительских устройствах на таких дисплеях меня всегда огорчали кричащие цвета, выбранные для рисования меню. Но когда попробовал нарисовать что-то сам, тогда понял, что по-другому и не получается. При попытке отобразить какие-то промежуточные цвета, на дисплее появлялась невероятная грязь. И что еще хуже, даже при небольшой смене угла обзора все цвета куда-то уплывали, сводя на нет любые попытки их точного подбора. Поэтому я надолго забыл про цветные дисплеи. Но однажды мне в руки попал небольшой IPS дисплей. Тут же его опробовал и понял, что это совсем другое дело. На нем можно нарисовать что угодно, и смотрится это именно так, как было задумано. На IPS дисплее цвета получаются очень похожими на те, что вижу на экране компьютерного монитора (тоже IPS).

Для данного устройства выбрал IPS дисплей с диагональю 2.4 дюйма, сделанный на контроллере ST7789. Он имеет интерфейс SPI. Такие дисплеи продаются в двух вариантах: в виде модуля на печатной плате, или отдельно только дисплей.



Во втором случае подключение дисплея делается с помощью разъема для сверхплоского кабеля на 10 контактов с шагом 1 мм. Оба варианта я купил и опробовал, дисплеи полностью идентичны.



Некоторое время не мог решить, какой вариант дисплея использовать. Если взять дисплей без платы, у него меньше габариты, появляется больше свободы в компоновке устройства. С другой стороны, дисплей на плате имеет металлическую рамку, которая его крепит. Это показалось более удобным. Но без доработок плата дисплея в корпус Z32 не влезала – ширина между стойками корпуса была недостаточной. Тем не менее, я все равно выбрал вариант с платой, хоть правильность данного выбора можно поставить под сомнение.

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


Процесс


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


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

Если применять дисплей с собственной платой, тогда он будет достаточно высоко приподнят над платой управления. При подключении через пару разъемов PLS – PBS расстояние между платами получится 11 мм (2.5 мм + 8.5 мм). Это слишком много, придется применять кнопки с длинными толкателями. Есть более низкие версии разъема PBS (5 мм), но все равно расстояние между платами получается слишком большим. Подходящее решение я увидел на платах старых жестких дисков. Там штырьки PLS проходили сквозь отверстия в плате, а на другой стороне был припаян низкий разъем PBS в SMD исполнении. Для него все равно, с какой стороны вставляются штырьки. Такие разъемы нашлись на Aliexpress (к сожалению, ссылка больше не работает). Данный вариант соединения плат позволяет получить между ними очень малое расстояние, вплоть до толщины пластика в разъеме PLS (2.5 мм).

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


Еще фото разъема



На плате дисплея с обратной стороны есть некоторые компоненты (самый высокий из них – разъем для шлейфа), поэтому расстояние между платами нельзя делать слишком малым. На плате управления хоть и нет компонентов под дисплеем, но могут быть пайки, которые тоже имеют какую-то высоту. В итоге выбрал расстояние 4 мм, длины штырьков PLS для этого как раз хватило. Плата дисплея имеет 4 крепежных отверстия для винтов М2. В качестве стоек применил вплавляемые втулки с накаткой и внутренней резьбой М2. Часть с накаткой имеет высоту как раз 4 мм, плюс есть еще шейка диаметром 3 мм и высотой примерно 1.5 мм. Для этой шейки в печатной плате сделал отверстия 2.9 мм, в которые втулки плотно запрессовались. Можно еще их припаять по кругу к площадкам, но это лишнее.


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


Теперь рамка дисплея плотно входит в отверстие в корпусе. На этом часто и останавливаются, но такое оформление дисплея выглядит кустарно.

Существуют два основных способа оформления ЖК-дисплеев. Первый – когда в передней панели делается прямоугольное отверстие по размеру активной области дисплея. Стекло дисплея плотно прилегает изнутри к панели.

Пример оформления 1


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

Пример оформления 2


Решил выбрать второй вариант, но как это повторить в домашних условиях? Единственный вариант, который удалось найти, это печать этикетки на самоклеющейся бумаге, на которую потом с помощью прозрачного двухстороннего скотча приклеивается стекло. Окно для активной области дисплея вырезается заранее. Это был первый опыт, получилось не совсем хорошо, но вполне сносно. Как и ожидалось, липкий слой двухстороннего скотча не совсем равномерный, заметны небольшие пятнышки. Хуже всего, что я заказал узкий двухсторонний скотч, поэтому видны стыки. С широким результат был бы лучше. Печатал на черно-белом лазерном принтере. Чтобы изображение не было излишне контрастным, вместо белого цвета использовал 50% серого.


Углубление в корпусе всего около 1 мм, поэтому стекло должно быть тонким. У меня такого не нашлось, заказал на Ozon. В описании лота было четко написано, что это оргстекло, но по факту оказалось что-то типа ПЭТ или поликарбоната. Запах при нагревании совсем не тот, при обработке тянется. Материал очень мягкий, поверхность быстро покрылась мелкими царапинами. В будущем стекло можно заменить.


Джойстик

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


Производитель и тип джойстика остались неизвестными. Поиск по картинке дал результат, что точно такой же джойстик использовался в телефоне Nokia 6600. Джойстик имеет 4 кнопки, которые срабатывают при наклонах. И еще одну центральную кнопку, гораздо более тугую, которая срабатывает при нажатии.


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



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

По высоте джойстик, конечно же, не совпал с дисплеем. Пришлось изготовить маленькую платку, которая впаивается в основную плату на штырьках PLS2 (с шагом 2 мм). Расстояние между платами – 2 мм. Вероятно, джойстик можно было бы разместить прямо на плате управления и согласовать с дисплеем, если бы тот был без своей платы. Но менять выбор относительно дисплея уже не хотелось.


Если плата расположена слишком близко к передней панели, то есть еще одна проблема – ее невозможно закрепить на обычных стойках, так как для стоек остается слишком мало высоты. Поэтому пришлось бы мудрить с креплением, что в итоге не принесло бы никакого выигрыша. А здесь расстояние от платы до панели достаточное (6 мм), можно использовать стойки. Крепежные отверстия пришлось разместить довольно близко к краям корпуса, поэтому стойки и винты большого диаметра не помещались. Решил для крепления платы использовать винты М2, как и для крепления дисплея. Обточил пластиковые стойки до диаметра примерно 5.5 мм, а внутрь тисками запрессовал латунные втулки с резьбой М2.


Компоновка

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


Термометр

Когда разработка была почти закончена, решил добавить в прибор термометр. Это более востребованный прибор, чем магнитометр. Ведь довольно часто возникает потребность проконтролировать температуру какого-то греющегося компонента. А тут будет порт USB, можно на компьютер записывать лог температуры. В результате предусмотрел еще один разъем и на основе микросхемы DS18B20 сделал внешний датчик температуры.


Питание

Всю жизнь я провел рядом с розеткой, поэтому в устройствах с автономным питанием особой нужды не испытывал. Наоборот, с ними одни хлопоты – их надо заряжать, следить за уровнем заряда. Для магнитометра хватило бы сделать разъем для внешнего питания, но я решил для разнообразия сделать полностью автономный прибор. На радиорынке купил Li-Ion аккумулятор емкостью 1400 мА*ч, чего должно хватить.


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



Корпус Z32 изначально имеет батарейный отсек: внутри есть съемная перегородка, а в задней панели – съемная крышка. Разъем аккумулятора я разместил на плате напротив этой крышки. При необходимости можно отключить аккумулятор, сняв только крышку. И даже можно вынуть и вставить сам аккумулятор.


Вместе с аккумуляторным питанием появляются дополнительные проблемы: аккумулятор надо как-то заряжать, надо следить за уровнем его заряда, надо как-то выключать питание, гарантируя низкий ток утечки. А также надо как-то получить нужное напряжение питания схемы. Датчик магнитометра, дисплей и процессор требуют напряжения питания 3.3 В. Напряжение заряженного аккумулятора может быть до 4.2 В, а в процессе разрядки оно снижается, и при 3.3 В остается сравнительно малый процент заряда. Поэтому напряжение можно только понижать. Применять здесь импульсный стабилизатор не имеет смысла, потому что разность входного и выходного напряжений очень небольшая. Если принять напряжение аккумулятора в середине цикла разрядки за 3.85 В, то КПД линейного стабилизатора составит 85%, что соизмеримо с импульсным. В качестве стабилизатора решил использовать микросхему RT9193-33GB, которая была под рукой.

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

В первом случае возможна реализация часов реального времени, процессор периодически может просыпаться. Тогда и включение питания можно реализовать чисто программно. Но есть проблема питания самого процессора. Если он может питаться напрямую от аккумулятора или батарейки, тогда проще, я когда-то делал такое на AVR, где диапазон напряжений питания шире. Но в данном случае 4.2 В аккумулятора слишком много. А постоянно работающий стабилизатор питания – это лишнее потребление, сам RT9193-33GB потребляет 100 мкА, что для режима ожидания очень много.

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

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

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


Джойстик подключается через разъемы XS5, XS6, общий вывод его кнопок подключен к земле. Кнопки LEFT, RIGHT, UP, DOWN подключены напрямую к портам процессора. А центральная кнопка BTN через диод Шоттки VD1 подключена к схеме включения питания. При нажатии этой кнопки открывается транзистор VT1, который подает напряжение аккумулятора VBAT на вход EN стабилизатора U2. Стабилизатор при этом включается и подает напряжение питания на всю схему. Но прибор по-прежнему прикидывается выключенным: дисплей не зажигается. Если в это время кнопку отпустить, то питание снова выключится. Но если кнопку удерживать дольше 2 сек., то процессор включит дисплей и откроет транзистор VT2, который будет удерживать питание включенным. Если теперь кнопку отпустить, прибор останется включенным. Чтобы эта же кнопка BTN могла делать отключение питания и выполнять какие-то другие функции, она через второй диод сборки VD1 подключена к порту процессора.

В качестве транзистора VT2 применен так называемый «цифровой транзистор». Довольно удобная штука, но почему-то не сильно популярная у радиолюбителей. Такие транзисторы очень часто встречаются в японской аппаратуре, их можно найти на старых платах. Да и в продаже они есть, причем цена не сильно выше обычных транзисторов. А место на плате экономят – тот же корпус SOT-23, но внутри уже есть два резистора. Один параллельно переходу БЭ, второй – последовательно с выводом базы. В результате на базу такого транзистора можно напрямую подавать логический сигнал.

Для выключения питания надо удерживать ту же кнопку, но теперь уже требуется интервал 4 сек., по истечению которого процессор выключит дисплей и снимет управляющий сигнал с транзистора VT2. Питание все еще останется включенным, но пользователю будет казаться, что устройство выключилось. Если теперь отпустить кнопку, питание будет снято.

Для алгоритма включения и выключения питания надо предусмотреть обработку нештатных ситуаций. Например, что должно происходить, когда кнопка включения питания зажата на длительное время? Например, она случайно нажалась, когда устройство находилось в кармане. По логике, устройство должно включиться, затем отсчитать задержку отключения и мирно выключиться. Больше ничего до отпускания кнопки не должно происходить. Такой тест на удержание кнопки не прошло ни одно из готовых устройств, имеющихся у меня. Осциллограф DSO2D12 циклически включался-выключался, а mp3-плейер SHMCI C5S залил экран зловещим белым цветом.

В идеале, когда кнопка включения случайно зажата, устройство должно отключиться полностью. Но это сложно реализовать схемотехнически, кнопка ведь подает питание на схему. Хоть дисплей и выключен, потребление в таком режиме составляет примерно 30 мА.

Задача питания автономных устройств в наше время является очень актуальной, таких устройств вокруг великое множество. К удивлению, мне не удалось найти исчерпывающего описания способов организации питания таких устройств. Есть только отрывочные сведения, а в остальном полный хаос, каждый делает как вздумается. Анализ доступных схем готовых устройств, типа mp3-плейеров и прочего, не внес особой ясности. Иногда схемы включения питания собраны на россыпи отдельных транзисторов, а иногда в таких схемах встречаются сложные специализированные микросхемы типа RT9941 и подобных. У всех сделано по-разному, а без алгоритма программной части порой сложно понять, что там было задумано.

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

С зарядкой аккумулятора тоже не все ясно. Самой доступной микросхемой контроллера зарядки является TP4056, ее и решил применить. Для зарядки использую порт USB с разъемом Type-C. Этот порт одновременно предназначен и для обмена данными с компьютером. Ток зарядки я решил ограничить на уровне 400 мА, чтобы вписаться в спецификацию с максимальным током 500 мА. Для этого резистор R_prog выбран 3 кОм. Аккумулятор используется в буферном режиме, т.е. к нему одновременно подключено и зарядное устройство, и нагрузка. Мешать работе от аккумулятора контроллер зарядки не будет: когда входное напряжение становится ниже напряжения аккумулятора, он переходит в Sleep Mode, ток разрядки аккумулятора не превышает 2 мкА.

Такое подключение имеет недостаток – при включенном питании устройства зарядка никогда не закончится. На последнем этапе зарядки (этап CV) контролируется ток, и как только он упадет до 1/10 от заданного, зарядка считается завершенной. Но ток никогда не упадет до такого значения, так как включенное устройство потребляет больше (примерно 100 мА). Возможно, это даже логично – при работе устройства постоянно тратится заряд, его надо пополнять. Но готовые устройства с аккумуляторами (например, телефоны) ведут себя иначе: в какой-то момент они показывают полную зарядку аккумулятора и она прекращается. Чтобы такое реализовать здесь, при подключении кабеля USB питание должно подаваться от него напрямую, а не от аккумулятора. Для этого понадобятся какие-то ключи питания или специализированная микросхема. Все это усложняет конструкцию, делать этого не стал.

Микросхема TP4056 индицирует процесс зарядки и его окончание с помощью двух светодиодов. Красиво иметь один двухцветный светодиод, но тут он нужен с общим анодом, а это редкость. Поэтому в схему были добавлены два цифровых транзистора VT3 и VT4 для получения возможности использования светодиода с общим катодом.

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

Во всех устройствах с питанием от аккумулятора, как правило, индицируется степень его зарядки в процентах. В теории, надо считать величину заряда в кулонах – сколько вошло в аккумулятор при зарядке, сколько вышло при разрядке. Но чтобы показать проценты, надо знать емкость, т.е. требуется калибровка. А это хлопотное дело, никто им заниматься не будет. Поэтому остается классический способ – определять процент зарядки по напряжению. Понятно, что это напряжение много от чего зависит (температуры, величины тока аккумулятора, его емкости, конкретного типа и т.д.), зато этот способ самый реальный. Нашел в Сети множество зарядных и разрядных графиков для Li-ion аккумуляторов, и все они разные. Взял наиболее понравившийся. В идеале, конечно, надо было снять такой график именно для моего аккумулятора и именно с моим значением тока зарядки и разрядки. Но не слишком ли много этот проект хочет?


Форма разрядной кривой имеет участки с разной крутизной. Сначала напряжение быстро падает, затем идет долгий пологий участок, а в конце разрядки оно опять резко падает. Решил сделать индикацию двумя способами – символом батареи с черточками и цифрами в процентах. Количество черточек взял 5, каждая соответствует 20% заряда. Для каждой из этих черточек по графику определил пороговое значение напряжения. А для вывода в процентах просто линейно интерполирую в интервале между двумя черточками.


uint8_t TAdc::GetCharge(void)
{
  //State Of Charge:
  const uint16_t SOC[] =
  //  0%,  10%,  20%,  30%,  40%,  50%,  60%,  70%,  80%   90%, 100%
  { 3200, 3680, 3730, 3770, 3800, 3830, 3870, 3940, 4020, 4110, 4200 };

  uint8_t i;
  uint16_t v1, v2;
  for(i = 0; i < 10; i++)
  {
    v1 = SOC[i];
    v2 = SOC[i + 1];
    if(Vacc <= v2) break;
  }
  if(Vacc < v1) return(0);
  if(Vacc > v2) return(100);
  return(i * 10 + (Vacc - v1) * 10 / (v2 - v1));
}

Процессор питается от аккумулятора через стабилизатор на 3.3 В, поэтому для измерения напряжения требуется отдельная цепочка в виде делителя напряжения (так как напряжение аккумулятора выше напряжения питания процессора). Но такой делитель будет постоянно разряжать аккумулятор. Чтобы избежать этого, делитель надо сделать отключаемым (вместе с питанием самого устройства). В схеме, которая была выше, этот делитель (R2R3) подключается тем же ключом, который используется для включения стабилизатора. Чтобы снизить падение на ключе (и погрешность измерения), в качестве VT1 использован MOSFET. Иначе для включения стабилизатора можно было бы поставить обычный биполярный транзистор.

Поскольку в конце процесса разрядки напряжение аккумулятора приближается к 3.3 В и может стать даже ниже, напряжение питания процессора тоже снизится. Это напряжение поступает и на ножку VDDA, которая является входом опорного напряжения для АЦП. Получается, что в конце разрядки аккумулятора АЦП будет врать. Чтобы этого не происходило, дополнительно измеряется напряжение внутреннего опорного источника (в STM32F103 оно равно 1.2 В), на его основе вычисляется реальное напряжение питания VDDA, а затем значение напряжения на аккумуляторе.

Когда напряжение на аккумуляторе снижается ниже 3.3 В, схема еще некоторое время может нормально работать. Стабилизатор питания в такой ситуации переходит в режим bypass, пропуская на выход полное входное напряжение, за вычетом некоторого небольшого падения (порядка 60 мВ при токе 100 мА). Аккумулятор имеет встроенный протектор, который защищает его от глубокой разрядки. Обычно нижний порог составляет около 2.9 В. Но доводить до срабатывания этой защиты нельзя, поэтому процессор должен следить за уровнем питания и выключать его в случае снижения ниже критического значения. Для этого у процессора есть Programmable voltage detector (PVD).

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

Процессор

В качестве процессора использовал распространенный и дешевый 32-разрядный микроконтроллер STM32F103C8T6 (U4). Работает он от встроенного RC-генератора (HSI), тактовая частота выбрана 64 МГц. Для прошивки и отладки на плате предусмотрен разъем SWD (XP2).


Интерфейс USB

Несмотря на то, что процессор имеет встроенный физический порт USB, я все равно использую внешний мост USB-UART. Обычно я делаю гальваническую развязку USB, там наличие моста оправдано. Но в данном случае устройство питается от USB, поэтому развязки нет. Но я все равно применил мост. Потому что разобраться с программированием USB – задача крайне трудная. Все эти дескрипторы, эндпоинты… Стоит ли оно того, если есть крошечная и дешевая CH340E, где уже все сделано до нас?


EEPROM

Для хранения различных настроек требуется энергонезависимая память. Нормальной EEPROM на борту в STM32F103 нет (что явный минус по сравнению с AVR). Можно как-то изловчиться и использовать FLASH вместо EEPROM. Но заниматься этим нет особой нужды, потому что существуют миниатюрные внешние микросхемы EEPROM в корпусе SOT-23-5. Такую здесь и применил (ZD24C08A).

Звук

Как правило, во всех приборах есть звуковой излучатель. Он нужен для формирования реакции на нажатия кнопок, для звуковой индикации ошибки, а также для формирования звуков включения-выключения. Обычно я использовал электромагнитные излучатели. Но в последнее время несколько раз натыкался на одну и ту же проблему с ними: при работе таких излучателей от источника питания потребляется значительный импульсный ток, что вызывает сильные помехи. Недавно попробовал альтернативу – пьезокерамические излучатели. Это хорошо забытое старое, когда-то единственно доступными были именно керамические излучатели семейства ЗП, которые стояли во всех будильниках. Современные излучатели приятно удивили очень низкой потребляемой мощностью, а линейность АЧХ у них оказалась даже лучше, звук довольно низкой частоты они воспроизводят неожиданно хорошо. В данном устройстве применил такой излучатель типа HPS12G.


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

При включении и выключении хотелось иметь не одиночный звуковой сигнал, а некую последовательность нот, мелодию. При включении тон должен быть восходящим, при выключении – нисходящим. Присоединил к проекту известный файл pitches.hpp, где занесены частоты всех музыкальных нот. Но какие именно это должны быть ноты? Что-то подобрал, но получилось не очень. Как оказалось, написание мелодий для микроконтроллерных устройств требует особых навыков.

Подбор кода


Разъемы датчиков

Для подключения AMR-датчика и термометра DS18B20 решил использовать одинаковые 4-контактные разъемы TRRS (tip, ring, ring, sleeve) диаметром 3.5 мм. Для датчика температуры достаточно трех проводов (или даже двух при питании от шины 1-Wire), хватило бы обычного 3-контактного TRS, но пусть разъемы будут одинаковые. Для разъемов на верхней части корпуса выфрезеровал выемку, в которую можно будет вклеить наклейку с подписями.


Когда разъем TRRS используется нестандартно, то можно выбирать назначение контактов как вздумается. Почему-то решил, что питание лучше подключить к S, а землю на ближайший R, как у гарнируры. Но это было роковой ошибкой, хорошо, что проверил заранее. В процессе втыкания штекера S и R кратковременно замыкались, что вызывало КЗ по питанию. Пришлось перенести питание на контакт T, т.е. на самый кончик штекера.

Для возможности горячего подключения AMR-датчика постоянно проверяется наличие ответа (ACK) по шине I2C. Если ответа нет, значит датчик не подключен. Когда датчик обнаруживается, в него посылаются данные для инициализации, а затем начинается циклическое считывание показаний.

Печатная плата

Когда со схемой все понятно, можно приступать к разводке печатной платы. Ее размеры диктуются размерами корпуса, они получились 94 х 60 мм. Перед разводкой необходимо пополнить библиотеки PCAD2006 компонентами, которые используются впервые. В этом проекте такие были. Все библиотеки я создаю сам, потому что в готовых творится полный бардак – разные толщины линий, разные шрифты, разный стиль компонентов.



Разводка такой платы – дело легкое, так как здесь куча свободного места. Большинство резисторов и конденсаторов взял типоразмера 0603, хотя можно было уместить и 0805. Для микросхемы контроллера зарядки предусмотрел полигон для охлаждения. Процессор имеет корпус с шагом выводов 0.5 мм, он подключен дорожками 0.3 мм с зазором 0.2 мм между ними. От ножек процессора дорожки должны тянуться через всю плату, при такой длине ширина 0.3 мм для ЛУТ маловата. Поэтому длинные дорожки сделал шириной 0.5 мм. Но как красиво с них перейти на 0.3 мм? Сделал через короткие сегменты промежуточной толщины, но место этого перехода, на мой взгляд, заметно портит эстетику платы. На заводской плате, конечно же, такого перехода не делал бы, а тянул дорожки 0.3 мм. Именно поэтому в цепи звукового излучателя поставил 2 резистора – чтобы с их помощью перейти на дорожки другой ширины.

Плата была отутюжена и вытравлена. Для ЛУТ на этот раз взял бумагу из другого глянцевого журнала, более блестящую на вид. И она гораздо лучше сработала, чем бумага из журналов «Stereo & Video».



Для переходных отверстий обычно использую сверло 0.7 мм, а потом расклепываю отрезки одножильного медного провода сечением 0.35 мм кв. Получаются практически плоские переходные, которые потом лудятся вместе со всей платой. Нижнюю площадку контроллера зарядки припаял к полигону с помощью фена сплавом Розе. Микроконтроллер с шагом выводов 0.5 мм паял под микроскопом с помощью жала «микроволна». Еще одним проблемным компонентом является разъем USB-C. У него тоже мелкий шаг выводов. Чтобы была возможность их припаять, рядом с разъемом желательно не размещать другие компоненты. Для крепления дисплея в плату запрессовал 4 втулки с резьбой М2. Маленькую платку с джойстиком впаял на штырьках PLS2. Стойки для крепления платы к корпусу сначала прикрутил к плате, а затем вместе с платой вклеил их в корпус. Плата при этом центрировалась с помощью дисплея, рамка которого плотно входит в окно.




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

Программа – кошмар не для всех

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

Датчик магнитного поля HMC5883L

Первым делом написал класс THMC5883L (файлы hmc5883l.cpp, hpp) для работы с датчиком HMC5883L. Здесь основная работа – это описать константы для всех внутренних регистров, чтобы в исходном тексте не было никаких магических чисел. Функционал класса не очень богатый: он умеет настраивать режимы датчика и читать из него измеренные значения и байты ID. Вот список публичных методов этого класса:

  THMC5883L(void);              //конструктор класса
  void Init(void);              //инициализация датчика
  void SetMode(uint8_t mode);   //установка режима датчика
  void SetRate(uint8_t rate);   //установка скорости считывания данных
  void SetAvrg(uint8_t avrg);   //установка количества усреднений
  void SetRange(uint8_t range); //установка диапазона измерения
  void SetMeas(uint8_t meas);   //установка режима измерения
  uint32_t ReadIdent(void);     //чтение ID
  int16_t ReadTemp(void);       //чтение температуры (только для HMC5983)
  Vector ReadRawXYZ(void);      //чтение результата
  bool Success(void);           //чтение стстуса обмена по шине I2C
Обмен по шине I2C ведется через шаблонный класс TI2Cio (файл i2cio.hpp). Реализация порта I2C – программная. Это дает полную свободу в назначении пинов, они передаются в качестве параметров шаблона. Пины, в свою очередь, управляются через другой шаблонный класс TGpio (файл gpio.hpp) с параметрами в виде номера порта и номера пина. Для формирования временных интервалов шины I2C используется функция задержки в циклах тактовой частоты Delay_ck() из класса TSysTimer (файл systimer.cpp, hpp). Работает она не совсем точно, но высокая точность здесь и не нужна.

Датчик могут отключить в любой момент, а затем подключить снова. Для контроля присутствия датчика проверяется наличие ответа ACK по шине I2C. С помощью метода Success() можно узнать, был ли ответ во время последней транзакции. Формированием флага занимается класс TI2Cio. При генерации условия Start флаг сбрасывается, а затем при записи каждого байта анализируется бит ACK. Если ответ каждый раз присутствует, флаг будет установлен. Если хотя бы в одной транзакции ответа нет, флаг будет сброшен. Если ответа нет, будут делаться попытки повторной инициализации датчика. Если она пройдет успешно, измерения будут возобновлены.

Векторные операции

Датчик HMC5883L выдает значения проекций вектора магнитного поля на оси X, Y, Z, измеренное значение представляет собой трехмерный вектор. Для таких величин написал шаблонный класс TVector (файл vector.hpp). Тип Vector, который используется для данных датчика, определен как TVector<int16_t>. При индикации измеренных значений требуется ряд векторных операций, поэтому класс TVector имеет несколько перегруженных операторов. Я не делал универсальный класс для работы с векторами (жалко времени). Например, всякие скалярные и векторные произведения здесь ни к чему. Реализованы только те операции, которые реально используются в данном проекте.

В первую очередь, используется присвоение векторов, с ними можно работать как с обычными переменными: Vector1 = Vector2. Этот оператор перегружен дважды, есть и такая форма: Vector1 = intX, когда всем компонентам вектора присваивается одинаковое значение. Также здесь используется сложение векторов Vector1 = Vector1 + Vector2 и деление всех компонент вектора на число: Vector1 = Vector1 / intX. Еще реализовано несколько специфических методов. Vector1.SetMin(Vector2) и Vector1.SetMax(Vector2) устанавливают минимальное или максимальное значение для каждой из компонент двух векторов. Это необходимо для отображения переменной составляющей (AC) поля. Еще есть метод Vector1.Module(), который возвращает длину вектора. Берется квадратный корень из суммы квадратов всех компонент вектора. Корень вычисляется с помощью итерационной формулы Герона. Метод Module() можно использовать только для 16-разрядных данных. Вопрос интересный, как в шаблонном классе взять для промежуточных результатов переменную вдвое шире, чем заданная параметром шаблона. Приходит на ум лишь писать специализации для каждого возможного типа.

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

AC и DC составляющие магнитного поля

Чтобы регистрировать переменную составляющую поля, данные с датчика надо получать как можно быстрее. Максимальная частота обновления данных составляет 75 Гц, здесь такая частота и используется. Чтобы получить переменную составляющую поля (амплитудное значение), вычисляется разность максимального и минимального значения каждой из компонент вектора, а затем находится длина получившегося вектора. Вычислять амплитуду колебаний длины вектора поля будет неправильно, потому что возможна ситуация, когда вектор вращается или колеблется, поле при этом будет переменное, а длина вектора – постоянная. Конечно, частота обновления данных датчика не очень велика, здесь в основном может идти речь про измерение переменных полей не выше промышленной частоты (50 Гц), да и то с оговорками. Формально частоты дискретизации не хватает, но благодаря некратной частоте семплирования все-таки можно получить близкое к правде амплитудное значение. Вполне нормально видны поля рассеяния трансформаторов, и даже поле сетевого провода с током.

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

Усреднение ведется в 2 этапа: сначала в окнах 80 мс, результат выводится столбиками. Значение поля DC на этом промежутке усредняется (интегрируется), а для поля AC вычисляется разница max и min (фактически, оно дифференцируется с выбором пикового значения). Затем эти значения усредняются еще раз в окне, длительность которого кратна 80 мс и задается из меню. Результат выводится в виде цифр.

В меню задается диапазон измерений датчика. Если величина поля выходит за этот диапазон, на дисплей выводятся черточки. Это означает, что надо перейти на диапазон повыше. Данный датчик имеет полный диапазон 8.1 Гс, поэтому он пригоден для измерения только слабых магнитный полей, типа паразитных полей рассеяния. Какие-то мощные постоянные магниты им измерить нельзя.

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


Датчик температуры DS18B20

Еще один датчик – это датчик температуры DS18B20. Он подключен по шине 1-Wire, которая эмулируется с помощью аппаратного UART. В отличие от синхронного интерфейса I2C, где по отдельной линии передается тактовый сигнал, и временные параметры могут варьироваться в широких пределах, интерфейс 1-Wire является асинхронным, у него нет отдельной тактовой линии. Поэтому тут должны строго соблюдаться временные интервалы. И они не слишком маленькие. Например, импульс сброса, с которого всегда начинается обмен, длится порядка 480 мкс. Поэтому лучше формирование критичных интервалов времени делать аппаратно. Интерфейс UART, конечно, не предназначен для реализации шины 1-Wire, но его смогли приспособить для этого очень давно.

Обмен по шине 1-Wire можно разбить на отдельные операции. Такими операциями может быть генерация сигнала сброса и прием импульса присутствия устройства, а также операция записи-чтения байта данных. Для выполнения этих операций был написан абстрактный класс TOwpAction (файлы therm.cpp, hpp). Он сам не знает, что будет делать, потому что содержит виртуальный метод Start(). Этот метод определяется в классах-потомках. Таких потомков два: TOwpReset для формирования сброса и TOwpRW для чтения-записи. Байт для записи в термометр передается как параметр конструктора, а принятый байт можно считать из public переменной Value. Для формирования последовательностей операций служит класс-контейнер TOwpTask. При создании указываем количество действий, а потом наполняем его указателями на действия, которые создаются прямо тут же, сразу с нужными байтами для передачи. Выглядит это так:

  OwpReadTherm = new TOwpTask(5);
  OwpReadTherm->AddAction(new TOwpReset());   //RESET
  OwpReadTherm->AddAction(new TOwpRW(0xCC));  //SKIP ROM
  OwpReadTherm->AddAction(new TOwpRW(0xBE));  //READ SCRATCHPAD
  OwpReadTherm->AddAction(new TOwpRW());      //READ TL
  OwpReadTherm->AddAction(new TOwpRW());      //READ TH

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

Последовательностей может быть несколько разных (например, запуск измерения и чтение результата), еще надо формировать задержку на время преобразования (она у данного датчика 750 мс). Этим занимается класс TTherm, который и предоставляет основной программе интерфейс термометра. Остальные классы – только для внутреннего использования. Интерфейс очень простой: конструктор класса, метод Execute(), через который, говоря образно, происходит питание процессорным временем, функция перезапуска измерения, флаг обновления температуры и сама температура в десятых градуса:

  TTherm(void);
  void Execute(void);
  void Restart(void);
  bool Update;
  int16_t Value;

Поскольку передача одного байта по UART передает только один бит на шину 1-Wire, для передачи байта требуется 8 посылок UART. Их формирует прерывание UART. Вообще, лишние прерывания в системе крайне нежелательны, они могут помешать действительно критичным к времени процессам. А здесь как раз тот случай, когда без прерываний можно обойтись. Достаточно программно делать поллинг флага окончания передачи UART и посылать в него следующие данные. Для 1-Wire критичны лишь некоторые интервалы (например, время от начала тайм-слота до момента чтения бита), а задержка между двумя тайм-слотами не имеет значения. Надо будет сделать такую улучшенную версию без прерываний.

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

Окно вывода результатов измерений термометра показано ниже:


АЦП

Для определения степени зарядки аккумулятора используется встроенный 12-разрядный АЦП. Про алгоритм его работы было написано выше. Все необходимые функции реализованы в классе TAdc (файлы adc.cpp, hpp).

Цветной графический дисплей

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

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

Чтобы замена контроллера не затрагивала библиотеки рисования графики, решил разбить работу с дисплеем на несколько классов. Функции управления дисплеем реализует класс TTft, описание которого находится в файле tft.hpp. А реализация может быть в разных файлах, она зависит от типа контроллера. В данном случае это файл st7789.cpp. Сама графическая библиотека – это класс TDisplay (файлы display.cpp, hpp). Конечно, она очень неполная, там есть только те функции, которые реально использую. Функции передачи команд и данных в дисплей вынесены в отдельный класс TSpiTft (файлы spitft.cpp, hpp). Название неудачное, потому что дисплей может подключаться не только по SPI. Но заранее делать универсально не стал, сделаю при первой необходимости.

Обычно при работе с дисплеем сразу всплывает вопрос использования DMA. У меня это пока не используется. Самым очевидным является следующий подход: рисуем в памяти, потом копируем в дисплей через DMA. Но реализация этого способа здесь невозможна. Чтобы организовать в памяти копию экрана 320 х 240 точек в режиме RGB565, потребуется 150 кбайт памяти, а в контроллере всего 20 кбайт. К тому же, такой подход хорош когда надо обновлять сразу весь дисплей – слайд-шоу, показ видео. В реальных приборах перерисовывается лишь небольшая часть экрана. Например, здесь перерисовываются лишь сами цифры, а все подписи остаются статичными. Но есть небольшие области, где выводятся шкалы, которые должны перерисовываться очень быстро. Если рисовать весь экран, то можно и не успеть.

Можно при выводе текста рисовать символ в небольшом буфере в памяти, а затем дать задание DMA копировать его в дисплей. В это время в другом буфере рисовать следующий символ, после чего программа должна ждать окончания предыдущей транзакции DMA. Символы могут быть большими (как здесь при выводе температуры), тогда каждый из них придется передавать по частям. Перед выводом каждого символа устанавливается окно вывода, а это требует передачи команд, что требует переключения вывода DC. Делать это DMA не умеет. Так будет ли выигрыш? Да, какой-то будет. Но перед этим надо потратить немало времени своей жизни на реализацию всего этого. Причем за это никакой благодарности не будет – устройство как работало, так и будет продолжать работать, потому что оно и сейчас успевает перерисовывать дисплей.

Создание шрифтов

На дисплей в основном выводятся цифры, а также подписи единиц измерения и названия режимов работы. Я очень не люблю, когда при выводе цифр длина строки прыгает, поэтому использую моноширинные шрифты. Но само начертание шрифтов требуется разное. На компьютере моноширинные шрифты используются редко, в основном пропорциональные. Современные шрифты рассчитаны на вывод с использованием сглаживания, без него они смотрятся плохо. Поэтому приходится их перерабатывать. Растеризацию True-type шрифта лучше доверить хорошему графическому редактору. Потом из готовой черно-белой картинки надо сделать массив. На этом этапе хорошо бы иметь возможность вносить небольшие попиксельные правки. Например, нет смысла делать весь шрифт шире из-за какой-нибудь буквы «W». Или делать его выше из-за сильно торчащего вниз хвостика буквы «q». Не нашел, в какой программе это удобно делать, поэтому пришлось написать свой простенький графический редактор. Очень примитивный и медленный, но удобный для конкретно этой задачи. Он умеет переводить символы в текстовый формат (файл .hpp) и даже рисует справа в комментариях начертание символа, используя символ решетки.


С его помощью было создано несколько шрифтов разного размера и с разным начертанием символов. Кроме шрифтов в этом редакторе можно создавать небольшие монохромные картинки (иконки).

В программе можно использовать сразу несколько шрифтов. Есть функция SetFont(), которой в качестве параметра передается имя шрифта. Из файла шрифта читается высота и ширина символов, а также общее количество символов. Многие шрифты сделаны как чисто цифровые. Кроме цифр там еще есть точка, минус, пробел. Чтобы в исходнике использовать коды этих символов в ASCII, для цифровых шрифтов при выводе делается подстановка реальных кодов этих символов. Для задания отступа между символами и между строками имеются отдельные параметры.

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

Звук

Формированием звуковых сигналов занимается класс TSound (файлы sound.cpp, hpp). Таймер микроконтроллера генерирует два противофазных меандра на своих выводах. Частота меандра рассчитывается согласно желаемой высоте тона. Длительность звучания формируется с помощью программного таймера.

После инициализации выходы должны находится в одинаковом состоянии. Иначе будет слышен щелчок. После окончания звукового сигнала выводы также устанавливаются в одинаковое состояние.

Клавиатура

Для управления устройством служит джойстик, который заменяет собой 5 кнопок. Обслуживанием кнопок занимается класс TKeyboard (файлы keyboard.cpp, hpp). Здесь производится опрос кнопок и подавление дребезга. Для этого анализируется состояние кнопок. Если оно неизменно в течении DEBOUNCE_TM = 50 мс, то считается, что дребезга нет. После этого формируется код нажатой кнопки и помещается в публичную переменную Message. Код может быть считан другими модулями. Если код кем-то обработан, то он сбрасывается, чтобы не достался больше никому. Если кнопку удерживать дольше времени HOLD_DELAY, то к коду добавляется модификатор удержания KEY_HOLD. Если удерживать еще дольше (LONG_DELAY), то добавляется KEY_LONG. Здесь это используется для включения и выключения питания. При отпускании кнопки тоже передается ее код, но с модификатором KEY_REL.

EEPROM и параметры

Для хранения энергонезависимых данных используется внешняя микросхема EEPROM типа 24C08. Она имеет интерфейс I2C, который реализован в классе TI2Cio. Это второй порт I2C с отдельными назначенными выводами. Программных I2C можно делать сколько угодно, объявляются они очень просто:

typedef TI2Cio<Pin_SDA1_t, Pin_SCL1_t> I2CbusEEPROM; //порт EEPROM

Работа с EEPROM реализована в классе TEeprom (файлы eeprom.cpp, hpp). Этот класс тесно связан с шаблонным классом TParam (файл param.hpp), который реализует параметр любого типа с возможностью энергонезависимости. Параметр описывается, напрмиер, так: TParam<uint8_t> MyParam; Если бы это была просто переменная, то в начале программы ее инициализировали бы каким-то значением. Для инициализации параметра есть специальные методы Init() и EEInit(). Если используется Init(), то это мало отличается от инициализации обычной переменной. Только вместе с начальным значением передается еще и минимальное и максимальное значение. Присвоить параметру что-то выходящее за эти пределы не получится. Если используем EEInit(), этим даем понять, что параметр будет энергонезависимый. При этом в EEPROM автоматически резервируется место и присваивается адрес. Дальше делается попытка чтения сохраненного параметра по этому адресу, если сигнатура EEPROM верна. Дальше этот параметр можно использовать в программе как обычную переменную, операторы присвоения у него перегружены. Если нужно в какой-то момент сохранить значение в EEPROM, то можно вызвать метод EESave(). Кроме того, параметр умеет сам себя редактировать с проверкой диапазона. Можно вызвать методы Inc(), Dec(), а также EEInc(), EEDec(), когда сразу произойдет и сохранение нового значения. Сервис при работе с параметрами легко наращивать. Например, можно сделать редактирование параметра с переменным шагом и прочее.

Класс TEeprom получился довольно громоздким из-за поддержки микросхем EEPROM разного объема. У них разный размер страницы, а также разная адресация. Начиная с 24С32 по I2C надо передавать дополнительный байт адреса. Выбор типа микросхемы делается в файле eemap.hpp. Там же описаны размеры страниц. В этом файле есть также карта адресов EEPROM, куда можно добавить переменные, для которых хочется иметь фиксированный адрес. Остальные адреса выделяются динамически при инициализации параметров. Естественно, при каждом старте программы это выделение проходит одинаково, в порядке вызова методов EEInc() для всех параметров.

Системный таймер и программные таймеры

Для формирования временных интервалов используется таймер SysTick, который является частью ядра процессора. Работа с таймером реализована в классе TSysTimer (файлы systimer.cpp, hpp). Таймер настроен на интервал 1 мс, с таким периодом возникают прерывания. В них инкрементируется 32-разрядный счетчик тиков. В начале основного цикла программы (файл main.cpp) надо вызывать функцию TSysTimer::Sync() для синхронизации системных тиков. После этого в любом месте основного цикла можно использовать флаг TSysTimer::Tick, который будет равен 1 только в начале каждого тика. При желании можно включить еще и секундные тики, когда флаг TSysTimer::SecTick будет устанавливаться в начале каждой секунды.

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

  • Plain – обычный таймер, после переполнения все время будет возвращать 1;
  • Oneshot – при переполнении только один раз возвращает 1;
  • Autoreload – при переполнении один раз возвращает 1, затем перезапускается.


Начальная инициализация системы

Из посторонних файлов в проект включен только файл startup_stm32f10x_md_vl.s (не считая файла с описанием регистров процессора stm32f10x.h). После сброса вызывается функция SystemInit(), которая реализована в файле sysinit.cpp. В ней я настраиваю тактирование, в данном случае от внутреннего RC-генератора HSI. Его частота умножается с помощью PLL до 64 МГц. Для Flash-памяти задаются циклы ожидания 2WS. В конце включается тактирование всех портов и они настраиваются на ввод с подтяжкой вниз. Чтобы все порты работали, отключается JTAG и делается remap портов PD0 и PD1.

Общее управление

Класс TControl (файлы control.cpp, hpp) реализует общее управление. Он пользуется другими классами и создает при инициализации все необходимые объекты. Метод Execute() класса TControl – это главное наполнение основного цикла программы. В этом методе вызываются методы Execute() других объектов, которым требуется процессорное время. Код кнопок считывается и обрабатывается. Выполняется ряд сервисов: отображения статуса устройства, считывания АЦП, считывания магнитометра и термометра, а также сервис автоотключения. Который совмещает выключение по таймеру и выключение при снижении напряжения питания. Сервисы используют программные таймеры для задания периодичности обновления значений на дисплее.


Порт

При управлении прибором существует некое двоевластие. С одной стороны, прибор слушается джойстика. Но такие же команды могут поступать и с компьютера. Для обмена используется протокол Wake. Формируются пакеты с уникальным символом в начале, затем идет адрес (необязательное поле), длина пакета, данные, контрольная сумма. Чтобы избежать использования уникального символа внутри пакета, используется byte stuffing. Реализован протокол в классе TWake (файлы wake.cpp, hpp). Для сборки пакета используются методы AddByte(), AddWord(), AddDWord(). Точно так же пакет и разбирается на отдельные параметры, для этого есть методы GetByte(), GetWord(), GetDWord(). Возможна передача массивов данных, для этого есть методы AddData()/GetData().

От класса TWake наследуется класс TWakePort (файлы wakeport.cpp, hpp), который делает привязку протокола к определенному физическому порту, в данном случае UART. Прием и передача пакетов ведется в прерываниях UART, которым назначен низкий приоритет.

Каждое устройство имеет свой определенный набор команд. Его реализация находится в классе TPort (файлы port.cpp, hpp). Он не имеет никаких интерфейсных функций (кроме стандартной Execute()), а сам берет данные, которые запрашивает компьютер. Или меняет их. Поэтому он должен иметь доступ практически ко всему. В данном проекте достаточно напроситься в друзья (friend) к классу TControl, все другие объекты управления принадлежат ему, доступ появится автоматически.

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

Заключение

Устройство готово. Но вместе с радостью завершения проекта есть и некая грусть – уж больно много времени и сил стали забирать проекты.
Добавить в избранное
+21 +26
свернутьразвернуть
Комментарии (15)
RSS
+
avatar
0
  • Barmale
  • 22 января 2026, 00:38
Комментарий ожидает проверки администрацией сайта. Подробнее...
+
avatar
0
  • Chatter
  • 22 января 2026, 00:50
Сейчас снова возрос интерес к аналоговому аудио.
+
avatar
+2
  • Leoniv
  • 22 января 2026, 01:00
Поскольку я нахожусь внутри этого процесса, то могу объективно оценить, yes или no. Желающих купить магнитофоны и какие-то аксессуары к ним — предостаточно. И цены держатся на высоком уровне, даже растут.
+
avatar
+2
  • infino
  • 22 января 2026, 01:03
И цены держатся на высоком уровне, даже растут.
Цены такие, что мом планы по покупке бобинника, все переносятся и переносятся.
+
avatar
0
  • Leoniv
  • 22 января 2026, 01:19
К какой модели присматриваетесь?
+
avatar
0
Что весьма странно.
Раньше требовалось куча механики, и ещё больше электроники. Сейчас механику можно серьёзно упростить за счёт умного управления прямым приводом, а электроника так и вовсе обесценилась. За что компании дерут космические цены, лично мне не понятно.
+
avatar
+1
  • infino
  • 22 января 2026, 00:55
Как всегда, эпично и наглядно — именно так, как и должно быть с точки зрения подхода к изделию. Пока прочитал по диагонали, но сделаю кофе и перечитаю всё ещё пару раз вдумчиво.
+
avatar
0
сейчас снова возрос интерес к аналоговому аудио
мне кажется это придумали сами любители аналогово аудио. А девайс хорошо спроектирован. Плюс
+
avatar
0
  • Leoniv
  • 22 января 2026, 01:21
Спасибо.
А про аналог — это не придумка, а реальность. Крутятся вполне реальные деньги, штампуются винилы, выпускаются проигрыватели, выпускается лента и.т. Даже Studer-Revox недавно выпустил на рынок новый магнитофон.
+
avatar
0
Забыли указать провода из бескислородной меди. Там вообще нереальные деньги крутятся.
+
avatar
0
  • Leoniv
  • 22 января 2026, 01:34
Да, там тоже крутятся.
+
avatar
0
Почти вся электротехническая медь безкислородная. Это часть тех процесса получения 90%+ чистоты меди.
+
avatar
0
про аналог — это не придумка, а реальность
ну я постоянно тоже общаюсь в аудио среде. Значит у меня реальность другая)
штампуются винилы, выпускаются проигрыватели, выпускается лента
да. Но относительно цифровых девайсов это капля в море
+
avatar
0
  • Grad
  • 22 января 2026, 01:53
Спасибо за отличный обзор и проект!
+
avatar
0
Как всегда идеально, но я бы утопил бы разъёмы ближе низу, а на задней стенке сделал бы п-образный вырез (паз). Может быть даже с декоративной крышкой с тремя u‐образными пазами под провода.Тогда разъёмы сенсоров были бы под защитой корпуса и их практически не возможно было бы выломать. Да, вставлять сложнее, но кому щас легко.

Пы.сы. Barber pole своими корнями уходит в те времена когда цирюльники помимо стрижек предлагали услуги кровопускания, а столбы были то-ли рекламой услуги то-ли сушкой бинтов (читал давно и по диогонали). Подробности — en.wikipedia.org/wiki/Barber%27s_pole
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.