LGT8F328P: импортозамещение по-китайски (начало)

Китайская фирма Logic Green еще примерно в 2015 году взялась за выпуск усовершенствованных аналогов линейки ATmega88–328. В настоящее время выпущены контроллеры LGT8F88P/168P/328P, старший позиционируется для производства Arduino-аналогов. В народе их называют «клонами Arduino», что неверно: ни сами контроллеры, ни платы на их основе клонами не являются, так как существенно отличаются от оригинала.

Однако, LGT8F328P и платы на его основе, если научиться правильно с ними обращаться, действительно могут заменить Arduino в большинстве применений, причем это потребует не очень большой возни. А если приложить усилия, то вы обнаружите, что LGT8F328P даже имеет довольно много преимуществ. Но приготовьтесь к трудностям: импортозамещение по-китайски означает, что большую часть времени вам придется потратить на угадывание «а что они тут имели в виду».

В этой статье не ставится задача показать все преимущества и особенности LGT8F328P. Мы далее попробуем только разобраться, как подключать некоторые более-менее распространенные платы, выполнять какие-то стандартные задачи, а также как использовать некоторые расширенные возможности LGT8F328P.

Так как статья получилась объемная, она разбита на две части: первая часть о простом программировании в Arduino IDE и вторая о программировании на низком уровне с обсуждением некоторых особенностей внутреннего устройства LGT8F328P.
Основные свойства и отличия LGT8F328P от ATmega328P указаны во множестве источников, приведу два, на мой взгляд, наиболее информативных:
https://alexgyver.ru/lessons/lgt8f328/
https://htrd.su/blog/2022/12/04/lgt8fx-lgt8328p/
Очень полезное и содержательное обсуждение, начавшееся в 2018 году, и продолжающееся по сей день: https://arduino.ru/forum/apparatnye-voprosy/obzor-klona-megi328-lgt8f328p.

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


Сразу скажем, что основная трудность при освоении LGT8F328P — во-первых, отсутствие каких-либо стандартов на различные версии плат, во-вторых, практически полное отсутствие документации на них (даже на китайском!). На сам контроллер документация имеется, хотя и довольно убого составленная, без многих подробностей и с ошибками. Китайский даташит переведен добровольцами на английский и даже на русский.

Английские переводы можно найти по ссылкам, указанным на страничке по адресу https://github.com/dbuezas/lgt8fx (в разделе «Docs & links»). Хотя размноженный на многих ресурсах русский перевод и требует основательного редактирования, но в отличие от английских самоделок, в нем заботливо переведено оглавление, что значительно облегчает поиск (см. файл LGT8F328P_ru.pdf в архиве, который вы можете скачать по адресу, указанному в конце статьи). Размещенный там файл мной дополнен разделом «Электрические параметры» — так как он отсутствует даже в китайском оригинале с официального сайта, то раздел был извлечен из описания предыдущей версии контроллера LGT8F328D. Также в нем исправлены некоторые ошибки в наименованиях регистров. Текст перевода не редактировался, хотя по совести, это следовало бы сделать.

Выбор платы


Хотя LGT8F328P может работать от 1,8 В (причем на всех частотах, вплоть до 32 МГц), все примеры далее приведены в расчете на 5-вольтовое питание, при непосредственном подключении выбранной платы к USB либо к внешнему источнику. О совмещении с 3-вольтоввыми схемами см. в следующей части.

Контроллер выпускается как минимум в трех вариантах корпусов — двух квадратных с 48-ю и 32-мя выводами с четырех сторон (QFP48/32L) и одном двухрядном с 20-ю выводами (SSOP20L). Соответственно, даже формально одинаковых по функциональности плат уже может быть три варианта, и вы их можете встретить на практике (см. рис. 1 ниже). Причем функциональность у этих версий одинаковая только теоретически — ядро контроллера одно и то же, но дополнительные выводы несут дополнительную функциональность. Сравните на рисунке число выводов плат с 48-выводными корпусами (два контроллера слева в нижнем ряду) и с двухрядным 20-выводным корпусом (крайний справа в верхнем ряду).

Любопытно, что на имеющихся в продаже микросхемах контроллеров Logic Green отсутствует какая-либо маркировка. Это не представляет трудностей при приобретении плат, маркированных их производителем (и имеющих обычно цвет, отличный от темно-синего у оригинального Arduino), но все-таки вызывает ощущение некоторой общей недоделанности.

op0trltjukhlnz30jjc9rh8k-j8.png
Рис. 1. Некоторые варианты модулей контроллера LGT8F328P

Чтобы жизнь медом не казалась, разные производители наплодили своих вариантов каждой платы. На рисунке представлены далеко не все возможные версии, здесь размещены в основном только чем-то выделяющиеся варианты и те, которые в последнее время можно без долгих поисков встретить в продаже. Из показанных мы будем рассматривать два аналога Nano (на рис. 1 две платы справа в нижнем ряду) — они встречаются в продаже довольно часто, основаны на 32-выводной версии контроллера (наиболее близкой к ATmega328), включают USB-конвертор и отличаются только наличием (крайний справа) или отсутствием (второй справа) внешнего кварца, задающего частоту. В большинстве случаев плата с наличием внешнего кварца имеет фиолетовое покрытие, без него — зеленое (повторим, что оригинальный Arduino Nano обычно имеет синий цвет и контроллер на нем маркирован логотипом Atmel или, возможно, MicroChip). Кварцевый резонатор, по которому можно отличить эти модификации, подключается к выводам 7 и 8 корпуса QFP32L и на фиолетовой плате расположен справа от кнопки над выводами GND и D2. Обычно этот кварц для совместимости с Arduino имеет номинал 16 МГц, но могут встретиться старые платы с резонатором 12 или 32 МГц.

Вот по этой ссылке:
https://github.com/nulllaborg/arduino_nulllab/blob/master/doc/LGT328P-LQFP32-Nano.pdf можно скачать схему подобного аналога Nano, близкого к тому, что мы будем использовать. Отличается она тем, что в современных модификациях установлен симметричный разъем USB-C (а не устаревший micro-USB), а также микросхемой адаптера — вместо традиционного для китайских клонов CH340 устанавливается его модификация CH9340, требующая специального драйвера (его можно скачать по ссылке http://www.wch-ic.com/downloads/CH9340SER_EXE.html). Полная разводка выводов выбранных нами плат с обозначениями дополнительных функций выводов приведена на указаной выше страничке https://alexgyver.ru/lessons/lgt8f328/.

Что требуется для начала работы?


Существует фирменная среда LGTSDK_Builder, интегрирующаяся с Atmel Studio, в которой можно создавать программы для LGT8F328P на «чистом» С.
Но мы рассмотрим более простой способ: BSP (аддон) для среды Arduino IDE. Среду лучше брать версии не старее 1.8.0 (все примеры далее опробованы именно в этой версии). Аддонов имеется несколько и не сразу можно понять, какой лучше подходит. По непостижимой причине и в торгующих организациях, и в интернет-описаниях, и среди авторов библиотек первоочередное внимание уделяется режиму LGT8F328P с внутренним тактированием, и попробовав запрограммировать выбранную нами плату, можно получить довольно обескураживающие результаты. Для наших вариантов, по совету Alexander Drozdov (см. ссылку на его блог в начале статьи), следует использовать BSP от Nullab. По этой ссылке есть подробная инструкция по установке через Менеджер плат Arduino. Заметим, что этот аддон, как и большинство остальных, основан на официальном BSP от производителя: https://github.com/LGTMCU/Larduino_HSP
.
В результате установки аддона от Nullab при выборе наиболее универсального варианта платы под названием DIY Board (Инструменты > Плата > Nulllab AVR Compatible Board > DIY Board) вы получите в разделе меню Инструменты дополнительные позиции (на рис. 2 подчеркнуты):

yyhquyktelzgvopssfzkzqyplei.png
Рис. 2. Дополнительные настройки платы DIY Board

Если вам необходимо использовать EEPROM, в LGT8F328P отсуствующий по умолчанию, то это можно сделать через данное меню. Но учтите, что EEPROM здесь эмулируется за счет программной flash-памяти, причем отбирает у программ вдвое больше по объему, чем объявленный размер EEPROM (каждый килобайт EEPROM равносилен уменьшению объема Flash на 2 Кбайта).

Обратим внимание также на скорость загрузки 57600 — она установлена специально, по некоторым сведениям на «умолчательной» скорости 115 200 прошивка не всегда срабатывает. По предположению автора, это связано как раз с отсутствием предварительной настройки тактирования через fuse-биты, так что незапрограммированный контроллер всегда сначала работает от внутреннего RC-генератора 32 МГц, калиброванного с точностью ±1%. Его стабильности достаточно для функционирования при не слишком больших скоростях, но на максимальной скорости возникают сбои. Заметим, что многие другие функции, связанные со временем, также будут работать неудовлетворительно, именно поэтому для работы следует выбирать вариант с внешним кварцем (фиолетовая плата).

Учтите, если у вас слетит программа-загрузчик, то восстанавливать его придется через специальный программатор, который можно приобрести отдельно; можно также использовать плату Arduino Uno или Nano, запрограммированные в качестве программатора (см. например, https://github.com/brother-yan/LGTISP). Подробнее этот вопрос мы будем обсуждать в следующей части статьи при рассмотрении программирования на ассемблере. Сейчас отметим только, что интерфейс низкоуровневого программирования LGT8F328P отличается от интерфейса AVR-контроллеров, которые программируются через SPI: здесь используется заимствованный у «взрослых» контроллеров загрузочно-отладочный интерфейс SWD (Serial Wire Debug) через специальные выводы SWC и SWD (отметим, что отечественные импортозаместители также заимствовали эту фичу).

Подробности

После первой загрузки программы, содержащей функции стандартного последовательного порта Serial, плата, снабженная обычным для Arduino Serial-загрузчиком, в некоторых случаях может выдать в Монитор порта кучу «мусора». Этот факт не очень мешает нормальной работе, так как «мусор» не появится, если запустить Монитор порта первым (до загрузки скетча), а также при повторных запусках, как самой платы, так и Монитора порта. Разобраться в причинах его появления не удалось — независимо от загрузчика (было перепробовано три варианта от разных источников) «мусор» вдруг может перестать возникать без видимых причин, никакой закономерности в этом не наблюдается.

Применение LGT8F328P для замены Arduino


Сначала можно попробовать реализовать на LGT8F328P некоторые задачи, обычные для Arduino. Сразу скажу, что никаких проблем при выполнении стандартных и некоторых из моих собственных примеров не обнаружено — плата (фиолетовая, с кварцем 16 МГц!) вполне годится для замены Arduino Nano.

Для проверки, все ли правильно работает, годится простая мигалка Blink из собрания примеров Arduino IDE (Примеры > 01.Basic > Blink). Встроенный светодиод должен замигать с частотой 0,5 Гц — ровно одна секунда горения и ровно одна секунда паузы, это означает, что вы правильно задали параметры платы в настройках по рис 2. Кстати, на зеленой плате без кварца, если вы никаких настроек не трогали, этот пример должен мигать вчетверо медленнее: по умолчанию контроллер настроен на внутренний RC-генератор 32 МГц с делителем 8, то есть на частоту 4 МГц.

Точно так же без проблем работает скетч Button (Примеры> 02.Digital > Button), который зажигает и гасит светодиод в зависимости от состояния кнопки, подключенной к порту D2 платы и к общему проводу GND. Открыв пример в редакторе Arduino, вы обнаружите, что встроенный подтягивающий резистор не используется. Следовательно при подключении замыкающей кнопки необходимо предпринять одно из двух: либо исправить в секции Setup() команду pinMode(buttonPin, INPUT) на pinMode(buttonPin, INPUT_PULLUP), либо подключить внешний резистор 1–10 кОм от вывода D2 к выводу 5V платы.

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

attachInterrupt(0, ledOnOff, FALLING); //срабатывание по спаду 

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

void ledOnOff(){
  digitalWrite(led_pin, (!digitalRead(led_pin))); }

Однако, если вы попробуете так сделать, быстро убедитесь, что программа неработоспособна — при нажатии на кнопку из-за дребезга светодиод будет переключаться совершенно бессистемно. Мало того, кнопка у нас подключена на замыкание с GND, т. е. правильная реакция при нажатии, как и указано в функции, по спаду уровня (FALLING), но из-за дребезга прерывание будет точно так же срабатывать и при указании положительного перепада (RISING).

Применим простой метод, основанный на запрещении прерываний после первого срабатывания и их разрешения с некоторой временной задержкой. Так как обычные Arduino-функции delay() и millis() внутри прерываний не работают, то мы применим системную функцию _delay_ms(). Для нее требуется подключить библиотеку util/delay.h (а также на всякий случай avr/io.h) и задать частоту тактового генератора (поскольку самостоятельно контроллер определить ее не в состоянии). В результате скетч (Button_Led.ino в архиве) будет выглядеть таким образом:

Button_Led.ino
#define F_CPU 16000000UL  // указываем частоту в герцах
   #include 
   #include 
const int buttonPin = 2;     // the number of the pushbutton pin
const int led_pin =  13;      // the number of the LED pin
void setup()
{
   pinMode(led_pin, OUTPUT);
   pinMode(buttonPin, INPUT_PULLUP);
   attachInterrupt(0, ledOnOff, FALLING); //срабатывание по спаду
}
void ledOnOff(){
   detachInterrupt(0);
   digitalWrite(led_pin, (!digitalRead(led_pin)));
   _delay_ms(500);  // ждем 0.5 сек.
   attachInterrupt(0, ledOnOff, FALLING); //срабатывание по спаду
}
void loop() {
}


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

Мигалка на Timer2


Проверим работу стандартных прерываний на примере прерывания переполнения Timer2. Демонстрационный пример (Proba_Timer2_Led.ino в архиве) попеременно выдает на выводах A4 и A5 частоту около 1 Гц (16МГц/256/256/256 = 954 Гц) и легко может быть переделан в генератор стабильной частоты в широком диапазоне с помощью изменения всего нескольких параметров в регистре TCCR2B. Частоту можно устанавливать точнее, манипулируя пороговым значением счетчика countT, отсчитывающего прерывания (в программе он считает от максимума до нуля, но можно поставить любое другое значение).

Для проверки работы этой программы на LGT8F328P подключите разноцветные светодиоды к выводам A4 (D18) и A5 (D19). Убедимся, что программа работает, как ожидалось. Можно также проверить функционирование «лишних» (по сравнению с Arduino Uno) портов A6 и A7, если подставить эти имена в программу и соответственно переподключить светодиоды.

Следует учесть, что обе глобальных переменных countT и flagKey здесь обязательно должны быть объявлены со спецификатором volatile — иначе программа будет работать с ошибками.

Программный UART


Проверку работы библиотеки SoftwareSerial удобно выполнить с использованием радиомодулей HC-12, представляющих собой беспроводной мост для связи двух или более плат Arduino через UART (об HC-12 я рассказывал здесь). Применять эти модули со встроенным Serial-портом не рекомендуется — UART по своей сути представляет собой соединение «точка-точка», в том числе и электрически, а в платах Arduino его выводы (RX и TX) уже подключены к микросхеме USB-адаптера. Если вы подключите туда же выводы RX и TX модуля HC-12, то рискуете, как минимум, стиранием загрузчика, а как максимум — выходом из строя платы Arduino. Поэтому для подключения к платам Arduino любых подобных модулей, соединяющихся через UART (Bluetooth, Wi-Fi, модулей GPS и т.п.) следует применять программный последовательный порт.

Отметим, что скетчи передатчика и приемника, приведенные далее, работают одинаково для стандартных Arduino и для плат на основе LGT8F328P — ничего специфического здесь нет. Потому вы можете их заменять друг на друга: выстроить передатчик на Arduino Uno, а приемник на LGT8F328P Nano или наоборот. Передачик в этих примерах будет запрограммирован выдавать в эфир примерно каждые 4 секунды двухбайтовое число 273. Перед передачей оно будет делиться на два байта (старший и младший), и потом опять собираться в целое на стороне приемника. Опыт показал, что делать это «руками» надежнее — не возникает возможных ошибок про передаче и сборке больших чисел, особенно со знаком минус. В последнем случае минус лучше передавать вообще отдельно, ASCII-символом ‘-‘, иначе функция print() наверняка запутается в толкованиях величины int в 8-разрядных системах и каноническом 32-разрядном языке С.

С учетом всех этих соображений программа передатчика будет выглядеть следующим образом (пример в архиве HC-12_proba_peredatchik_SoftSer.ino, в папке Soft_SERIAL):

Код передатчика
#define Ledpin 13
#define RX  8  //Определяем вывод RX (TX на модуле)
#define TX  9  //Определяем вывод TX (RX на модуле)
#include  // Библиотека программного порта
SoftwareSerial Serialpr(RX,TX); // Программный последовательный порт
word value=273; //условное 2-байтовое число
void setup() {
  Serialpr.begin(9600);
  pinMode(Ledpin,OUTPUT);
  delay(100);
}
void loop() {
  digitalWrite(Ledpin,HIGH);
  Serialpr.write(lowByte(value)); //передаем мл. байт 
  Serialpr.write(highByte(value)); //передаем ст. байт 
  delay(50);
  digitalWrite(Ledpin,LOW);
  delay(4000);
}


Загрузите этот пример в плату, выбранную для передатчика, подключите к выбранным выводам 8 (RX) и 9 (TX) выводы TX и RX (перекрестно!) модуля HC-12, и соедините его выводы питания с контактами 5V и GND платы. Передатчик можно подключить к отдельному адаптеру питания и отложить в сторону. Каждые 4 с передатчик будет мигать светодиодом, сообщая о посылке числа.

Скетч приемника (HC-12_proba_priemnik_SoftSer.ino в той же папке) окажется немного сложнее из-за необходимости сборки числа из отдельных байтов:

Код приемника
define Ledpin 13
#define RX 5 //Определяем вывод RX (TX на модуле)
#define TX 6 //Определяем вывод TX (RX на модуле)
#include  //библиотека программного порта
SoftwareSerial Serialpr(RX,TX);           
word value; //присланное число в буфере 
void setup() {
  Serialpr.begin(9600);
  Serial.begin(9600); //для отладки
  pinMode(Ledpin,OUTPUT);
}
void loop() {
    if (Serialpr.available() > 0) {
        // Пришла! Считываем её и анализируем
   byte bb = Serialpr.read(); //мл. байт
   Serial.println(bb,HEX); //для отладки
   value = Serialpr.read(); //ст. байт
   Serial.println(value,HEX); //для отладки
   value = value*256+bb; //присланное число
   Serial.println(value); //для отладки
   if (value==273) { 
//если равно посланному числу, то переключаем состояние LED
     digitalWrite(Ledpin,!digitalRead(Ledpin)); 
     }
  }
}


В этом случае выбраны выводы 5 (RX) и 6 (TX) — их следует соединить с приемным модулем HC-12, и подключив питание, оставить плату соединенной с USB-портом. Прием можно проверять, запустив монитор последовательного порта, а можно для краткости исключить все, относящееся в обычному Serial: при каждом приеме правильного числа светодиод будет переключаться.

Примеры использования расширенных функций LGT8F328P


Теперь попробуем задействовать дополнительные возможности LGT8F328P, отсутствующие в ATmega328P.

Timer3


Наличие еще одного 16-битного таймера (Timer3) — очень удобное свойство LGT8F328P. Напомним, что Timer0 занят системными функциями времени, Timer1 задействован библиотекой Servo и ШИМ-генерацией на выводах 9 и 10, Timer2 — функцией tone() и ШИМ-генерацией на выводах 3 и 11. В случаях пересечения с этими функциями дополнительный таймер оказывается совсем не лишний.

Для работы с Timer3 авторы аддона включили в поставку простенькую библиотеку MsTimer3 — модифицированный вариант библиотеки MsTimer2, которую легко найти на просторах интернета. Как и оригинал, библиотека MsTimer3 содержит всего три функции:

MsTimer3::set(unsigned long ms, void (*f)())  — задание временного интервала и имени функции (f) — обработчика прерывания.
MsTimer3::start() — включение таймера (разрешение прерываний по переполнению).
MsTimer3::stop() — остановка таймера.

Разрешение установки периода прерывания равно 1 миллисекунде, чего для большинства практических применений достаточно. Для каких-то особых целей можно отредактировать исходный код библиотеки MsTimer3 (его можно разыскать в пользовательской папке Arduino по адресу C:\Users\<имя пользователя>\AppData\Local\Arduino\packages). Там можно изменить прерывание переполнения на прерывание сравнения, что даст возможность более тонкой настройки периода — эти возможности авторы библиотеки оставили закомментированными.

Проверочный скетч для работы прерывания Timer3 очень простой — ниже приведен несколько упрощенный вариант примера из библиотеки MsTimer3 (в свою очередь, переработанный вариант из MsTimer2). Пример находится в архиве под названием Timer3_Ovfow_blink.ino:

Пример Timer3
#include 
#define led_pin 13 // default to pin 13
void flash()
{
  digitalWrite(led_pin, (!digitalRead(led_pin)));
}
void setup()
{
    pinMode(led_pin, OUTPUT);
    MsTimer3::set(1000, flash); // 1000 ms period
    MsTimer3::start();
}
void loop()
{
}


Как видите, основной цикл здесь оставлен пустым — в нем можно выполнять какие-то действия, тогда как прерывания будут выполняться независимо от них.
Если вы захотите обойти библиотеку и самостоятельно настроить Timer3, рекомендуется ознакомиться с описанием тонкостей его работы в разделе о программировании LGT8F328P на ассемблере во второй части этой статьи.

Цифро-аналоговый преобразователь


В LGT8F328P имеется 8-битный цифро-аналоговый преобразователь (ЦАП, DAC). Он удобен в случае необходимости синтезировать какой-либо аналоговый сигнал. Заметим, что выход ЦАП может служить входом компараторов (которых тут целых два, причем доработанных в сравнении с оригиналом от Atmel в части наличия входного гистерезиса), что позволяет обрабатывать различные комбинации цифровых и аналоговых сигналов на одном микроконтроллере.

Некоторый недостаток ЦАП в LGT8F328P — то, что на выходе там стоит голая резистивная цепочка R-2R, требующая дополнительного внешнего усилителя в случае работы на сколько-нибудь существенную нагрузку. Обычно достаточно подключить к выходу DAC0 (выход D4 платы) внешний ОУ по неинвертирующей схеме (см. неинвертирующий усилитель или повторитель в этой публикации). Причем обеспечив двухполярное (не менее ±7 В) питание этого ОУ и развязав выход ЦАП и вход ОУ через последовательный конденсатор (около 1 микрофарады, вход при этом необходимо привязать к «земле» резистором около 1 мегаома), можно получить переменное напряжение, симметричное относительно «земли».Если же двуполярный сигнал не требуется, то выход D4 и вход ОУ соединяются напрямую. Питание ОУ лучше, конечно, тоже обеспечить отдельное (не менее 7 вольт через аналоговый стабилизатор), но в простейшем случае можно просто соединить с питанием контроллера.

Работа с ЦАП очень проста, и требует всего два предварительных действия: явное задание источника опорного напряжения (в среде Arduino делается общим методом для АЦП и ЦАП) и включение DAC вместе с заданием режима контакта DAC0 (вывод D4 платы):
analogReference(DEFAULT); //что означает опорное = 5 В
pinMode(DAC0, ANALOG); //включаем ЦАП

Второй оператор выполнится быстрее, если задать режим непосредственно в управляющем регистре DAC:
DACON= 1<
Запуск преобразования с выдачей результата осуществляется командой аналогового вывода числа n в указанный вывод D4 (аналогично команде вывода ШИМ):
analogWrite(DAC0, n);
И эту команду можно выполнить существенно быстрее, если вывести непосредственно в регистр данных ЦАП:
DALR = n;
Посмотрим на практических примерах, как можно получить с помощью этого ЦАП некоторые стандартные сигналы.

Генератор «пилы»


В доступной документации нет ни слова про принцип работы и быстродействие модуля ЦАП, поэтому попробуем сначала оценить скорость работы, устранив все задержки по максимуму. Для этого используем показанный выше способ включения и задания числа непосредственно через регистры (пример в архиве Proba_DAC.ino):

void setup() { 
  DACON= 1<


Обратите внимание, что главный цикл loop() мы здесь не используем — вместо него организовали свой бесконечный цикл while(1) прямо сразу после задания параметров. В этом цикле байтовая переменная n с каждым отсчетом увеличивается от 0 до 255, затем автоматически сбрасывается в ноль и начинает возрастать заново. В результате получается «пила» с размахом, заданным опорным напряжением по умолчанию — при 5 вольтах размах «пилы» будет равен чуть больше 4 вольт (рис. 3):
y_m_qlrgylimlbyue6jsbajelpe.png
Рис. 3. Генератор пилообразного напряжения

Период «пилы» в этом случае оказывается около 140 мкс, т. е. частота около 7 кГц. Таким образом, одно преобразование при таком способе управления занимает примерно 140 мкс / 256 = 0,55 мкс (частота ~1,8 МГц). «Пила», как видим из осцилограммы, несколько искаженная, что частично обусловлено максимально возможной скоростью вывода результатов — при меньших частотах ее форма несколько выправляется.

Для снижения частоты вывода достаточно поместить внутри цикла while(1) рассчитанную задержку, например, с помощью системной функции _delay_ms (встроенный таймер Arduino не сработает, так как прерывания запрещены). Однако, частота снизится автоматически, если сделать все то же самое обычными функциями Ardiuno (пример Proba_DAC_onezig.ino):

   volatile uint8_t n;
void setup()
{ 
   analogReference(DEFAULT);
   pinMode(DAC0, ANALOG);
}   
void loop() {
   analogWrite(DAC0, n++);
 }


Из-за более медленных функций Arduino частота «пилы» снизится до 2,3 кГц (соответственно, на одно преобразование придется ~1,7 мкс), а форма ее несколько выпрямится, хотя и не полностью, что вызывает претензии к подгонке резисторов ЦАП. Дополнительно улучшить форму «пилы» можно, если снизить значение опорного напряжения: в analogReference вместо DEFAULT подставить INTERNAL2V048.

Отметим, что во втором случае мы перенесли оператор формирования «пилы» в функцию loop(), в соответствии с правилами составления программ Arduino. Если этого не делать, оставив искусственно придуманный бесконечный цикл while(1), как в предыдущем примере, то в картинке ничего не изменится.

Треугольник


Возрастающую «пилу» легко отобразить зеркально, превратив в ниспадающую — для этого в программах из предыдущего раздела достаточно оператор n++ заменить на n--. А если объединить обе разновидности, получим генератор треугольных колебаний (пример Proba_DAC_tri.ino):

   volatile uint8_t n;
void setup()
{ 
   analogReference(DEFAULT);
   pinMode(DAC0, ANALOG);
}
void loop() {
    while (n<255)
       analogWrite(DAC0, n++);
    while (n>=1)
       analogWrite(DAC0, n--);
 }


Результат на экране осциллографа будет примерно следующим (при использовании Arduino-функций частота следования «треугольников» немного менее 1 кГц, период чуть больше ~1 мс):
omi8ufaucafaby8djgysgioctfg.png
Рис 4. Генератор колебаний треугольной формы

Программируя этот пример, обращайте внимание на значения пределов во внутренних циклах while (n<255) и while (n>=1): при неправильном их указании может не произойти переход от одного к другому и будет работать только какая-то одна половинка «пилы».

Генератор синуса


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

Значения синуса следует рассчитывать со сдвигом примерно в половину диапазона 8-разрядного числа — тогда в таблице не окажется отрицательных значений, и результат работы ЦАП впишется в положительную область, соответствующую установленному опорному напряжению. В следующем примере (Proba_DAC_sin.ino) величина сдвига в таблице была принята равной числу 124:

volatile uint8_t n=0;
const PROGMEM uint8_t  sinewave[250]= // массив синуса
{124, 128, 132, 132, 136, 140, 144, 144, 148, 152, 156, 156, 160, 164, 168, 168, 172, 176, 180, 180, 184, 188, 188, 192, 196, 196, 200, 200, 204, 208, 208, 212, 212, 216, 216, 220, 220, 224, 224, 228, 228, 232, 232, 232, 236, 236, 236, 240, 240, 240, 240, 244, 244, 244, 244, 244, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 244, 244, 244, 244, 244, 240, 240, 240, 240, 236, 236, 236, 232, 232, 232, 228, 228, 224, 224, 220, 220, 216, 216, 212, 212, 208, 208, 204, 200, 200, 196, 196, 192, 188, 188, 184, 180, 180, 176, 172, 168, 168, 164, 160, 156, 156, 152, 148, 144, 144, 140, 136, 132, 132, 128, 124, 120, 116, 116, 112, 108, 104, 104, 100, 96, 92, 92, 88, 84, 80, 80, 76, 72, 68, 68, 64, 60, 60, 56, 52, 52, 48, 48, 44, 40, 40, 36, 36, 32, 32, 28, 28, 24, 24, 20, 20, 16, 16, 16, 12, 12, 12, 8, 8, 8, 8, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4, 8, 8, 8, 8, 12, 12, 12, 16, 16, 16, 20, 20, 24, 24, 28, 28, 32, 32, 36, 36, 40, 40, 44, 48, 48, 52, 52, 56, 60, 60, 64, 68, 68, 72, 76, 80, 80, 84, 88, 92, 92, 96, 100, 104, 104, 108, 112, 116, 116, 120};
void setup()
{ 
   analogReference(DEFAULT);
   pinMode(DAC0, ANALOG);
}
void loop() {
  n=0;
  byte ksin; // текущее значение синуса из таблицы
  while (n<250)
  {
    ksin=pgm_read_byte(&sinewave[n]); //значение из таблицы
    analogWrite(DAC0, ksin);
    n++;
  }   
}


Таблица ввиду довольно большого ее объема (250 байт) для экономии оперативной памяти помещается в память программ (PROGMEM). В данном случае это не имеет большого значения: оперативная память у нас почти не занята, а скорости извлечения практически одинаковые. Однако на всякий случай так следует поступать со всеми объемными массивами-константами, используемыми в Arduino — очень трудно точно рассчитать, какая часть ОЗУ будет занята, например, стеком во время работы программы.

Результат работы примера генерации синуса следующий:
dpnubvnqqbiuta4kuucngzxgbmu.png
Рис 5. Генератор синуса

Частота полученного синуса — около 1,7 кГц, что при 250 точках на период эквивалентно ~2,35 мкс на один отсчет.

Получение уникального идентификатора


Каждая микросхема LGT8F328P имеет свой уникальный 32-битный идентификатор, который можно прочесть через специальные регистры GUID3:0. Следует отметить, что в большинстве источников представлен ошибочный алгоритм чтения этого номера, потому приведем здесь два работающих способа. Пример в архиве носит название GUID.ino, один из приведенных далее вариантов в тексте примера закомментирован.
Первый способ (короткий) раз в секунду считывает 32-битное число целиком за одно действие:

void setup() {
  Serial.begin(9600);
}
void loop() {
  uint32_t guid= *(uint32_t*)&GUID0 ;
  Serial.println(guid,HEX);
  delay(1000);
}


Второй способ делает то же самое побайтно, что может быть удобнее для ряда применений:

void setup() {
  Serial.begin(9600);
}
void loop() {
  byte guid = GUID3;
  Serial.print(guid, HEX);
  guid = GUID2;
  Serial.print(' ');
  Serial.print(guid, HEX);
  guid = GUID1;
  Serial.print(' ');
  Serial.print(guid, HEX);
  guid = GUID0;
  Serial.print(' ');
  Serial.println(guid, HEX);
  delay(1000);
}


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

Во второй части, как говорилось, будем потрошить LGT8F328P на низком уровне.

Ссылка на архив с программами и документацией (на папку ASM пока не обращаем внимания!): туточки.

© Habrahabr.ru