Библиотека Генератора Ассемблерного Кода Для Микроконтроллеров Avr. Часть 1

Часть 2. Начало работы → Библиотека генератора ассемблерного кода для микроконтроллеров AVR



Часть 1. Первое знакомство

Добрый день, уважаемые хабровчане.

Хочу предложить вашему вниманию еще один (из множества имеющихся) проект по программированию популярных микроконтроллеров серии AVR. Мы могли бы потратить много текста, объясняя, зачем это нужно, но вместо этого мы просто рассмотрим примеры того, чем это отличается от других решений.

А все пояснения и сравнения с существующими системами программирования будут даны по мере необходимости в ходе разбора примеров.

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

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

Итак, давайте начнем.

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

Не будем отклоняться от устоявшейся практики и начнем с классического примера, своеобразного «Hello world» для микроконтроллеров.

А именно моргаем светодиодом, подключенным к одной из ножек процессора.

Давайте откроем VisualStudio от Microsoft (подойдет любая редакция) и создадим консольное приложение на C#.

Для тех кто не в курсе, достаточное для работы Community Edition абсолютно бесплатно.

Сам текст выглядит так: Пример 1. Исходный код

  
  
  
  
  
  
  
   

using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.PortB[0].

Mode = ePinMode.OUT; m.PortB.Activate(); m.LOOP(m.TempL, (r, l) => m.GO(l), (r) => { m.PortB[0].

Toggle();}); Console.WriteLine(AVRASM.Text(m)); } } }

Конечно, чтобы всё работало, нужна именно та библиотека, которую я представляю.

После компиляции и запуска программы мы увидим следующий результат работы этой программы в выводе консоли.

Результат компиляции примера 1

#include “common.inc” RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 L0000: in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL xjmp L0000 .

DSEG

Если скопировать результат в любую среду, способную работать с ассемблером AVR, и подключить библиотеку макросов Common.inc (библиотека макросов также является одним из компонентов представленной системы программирования и работает совместно с НаноRTOSLib ), то эту программу можно скомпилировать и протестировать на эмуляторе или реальном чипе и убедиться, что всё работает. Давайте подробнее рассмотрим исходный код программы.

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

Последняя строка программы фактически визуализирует результат всего написанного ранее в виде ассемблерного кода.

Все предельно просто и компактно.

И результат практически не отличается от того, что можно было бы написать на ассемблере.

К выходному коду может быть только два вопроса: первый — зачем инициализировать стек, если мы его все равно не используем и что xjmp ? Ответ на первый вопрос и одновременно объяснение, почему выводится именно ассемблер, а не готовый HEX, будет следующим: результат в виде ассемблера позволяет в дальнейшем анализировать и оптимизировать программу.

, позволяющий программисту выделять и изменять фрагменты кода, которые ему не нравятся.

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

Однако, если вам это не нравится, смело удаляйте его.

Для этого предназначен вывод на ассемблер.

Касательно xjmp является примером использования макросов для повышения читаемости вывода ассемблера.

Конкретно xjmp - замена для JMP И RJMP с правильной заменой в зависимости от длины перехода.

Если мы загрузим программу на кристалл, то моргания диода мы, конечно, не увидим, несмотря на то, что состояние вывода меняется.

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

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

Например, задержка в 0,5 секунды вполне подойдет: не слишком быстро и не слишком медленно.

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

Давайте изменим наше приложение следующим образом.

Пример 2. Исходный код

using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.PortB[0].

Mode = ePinMode.OUT; m.PortB.Activate(); m.WDT.Clock = eWDTClock.WDT500ms; m.WDT.OnTimeout = () => m.PortB[0].

Toggle(); m.WDT.Activate(); m.EnableInterrupt(); var loop = AVRASM.newLabel(); m.GO(loop); Console.WriteLine(AVRASM.Text(m)); } } }

Очевидно, что программа аналогична предыдущей, поэтому мы рассмотрим только то, что изменилось.

Во-первых, в этом примере мы использовали WDT (сторожевой таймер).

Для работы с большими задержками, не требующими особой точности, это лучший вариант. Все, что вам нужно для его использования, — это задать необходимую частоту, задав делитель через свойство WDT.Clock, и определить действия, которые необходимо выполнить при срабатывании события, определив код через свойство WDT.OnTimeout. Поскольку для работы нам нужны прерывания, их необходимо включить командой EnableInterrupt. Но основной цикл можно заменить пустынным.

В любом случае мы ничего в этом плане делать не планируем.

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

Если вам больше нравится LOOP, пожалуйста.

Результат не изменится.

Ну и напоследок посмотрим на получившийся код. Результат компиляции примера 2

#include “common.inc” jmp RESET reti ; IRQ0 Handler nop reti ;IRQ1 Handler nop reti ;PC_INT0 Handler nop reti ;PC_INT1 Handler nop reti ;PC_INT2 Handler nop jmp WDT ;Watchdog Timer Handler RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 ldi TempL, (1<<WDCE) | (1<<WDE) sts WDTCSR,TempL ldi TempL, 0x42 sts WDTCSR,TempL sei L0000: xjmp L0000 WDT: push r17 push r16 in r16,SREG push r16 in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL pop r16 out SREG,r16 pop r16 pop r17 reti .

DSEG

У тех, кто знаком с этим процессором, наверняка возникнет вопрос, куда делись еще несколько векторов прерываний.

Здесь мы использовали следующую логику — если код не используется, значит код не нужен.

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

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

Поэтому мы рассмотрим еще один способ, а заодно посмотрим, как в библиотеке организована работа с таймерами.

В кристалле Mega328, взятом за образец, их 3. 2 8-битных и один 16-битный.

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

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

Если взять тактовую частоту кристалла 16 МГц, то даже с максимальным периферийным прескалером в 8-битный счетчик не уместиться.

Поэтому не будем усложнять и воспользуемся единственным доступным нам 16-битным счетчиком Timer1. В результате программа принимает следующий вид: Пример 3. Исходный код

using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) {var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var bit1 = m.PortB[0]; bit1.Mode = ePinMode.OUT; m.PortB.Activate(); m.Timer1.Mode = eWaveFormMode.CTC_OCRA; m.Timer1.Clock = eTimerClockSource.CLK256; m.Timer1.OCRA = (ushort)((0.5 * m.FCLK) / 256); m.Timer1.OnCompareA = () => bit1.Toggle(); m.Timer1.Activate(); m.EnableInterrupt(); m.LOOP(m.TempH, (r, l) => m.GO(l), (r) => { }); Console.WriteLine(AVRASM.Text(m)); } } }

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

Основной текст программы – установка таймера на нужный режим.

Здесь для тактирования сознательно выбран прескалер 256, а не максимальный, так как при выборе прескалера 1024 для необходимой тактовой частоты 500мс, которую мы хотим получить, получается дробное число.

Итоговый ассемблерный код нашей программы будет выглядеть так: Результат компиляции примера 3

#include “common.inc” jmp RESET reti ; IRQ0 Handler nop reti ;IRQ1 Handler nop reti ;PC_INT0 Handler nop reti ;PC_INT1 Handler nop reti ;PC_INT2 Handler nop reti ;Watchdog Timer Handler nop reti ;Timer2 Compare A Handler nop reti ;Timer2 Compare B Handler nop reti ;Timer2 Overflow Handler nop reti ;Timer1 Capture Handler nop jmp TIM1_COMPA ;Timer1 Compare A Handler RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 outiw OCR1A,0x7A12 outi TCCR1A,0 outi TCCR1B,0xC outi TCCR1C,0x0 outi TIMSK1,0x2 outi DDRB,0x1 sei L0000: xjmp L0000 TIM1_COMPA: push r17 push r16 in r16,SREG push r16 in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL pop r16 out SREG,r16 pop r16 pop r17 reti .

DSEG

Комментировать здесь, кажется, больше нечего.

Инициализируем устройства, настраиваем прерывания и наслаждаемся программой.

Работа через прерывания — самый простой вариант создания программ реального времени.

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

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

Возможное решение — разделить код регистрации и обработки событий.

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

основная нить.

Ядро последовательно проверяет наличие необработанных флагов и при их обнаружении приступает к выполнению соответствующей задачи.

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

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

Пример 4. Исходный код

using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; m.PortB.Direction(0x07); var bit1 = m.PortB[1]; var bit2 = m.PortB[2]; m.PortB.Activate(); var tasks = new Parallel(m, 4); tasks.Heap = new StaticHeap(tasks, 64); var t1 = tasks.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); bit1.Toggle(); tsk.Delay(32); tsk.TaskContinue(loop); },"Task1"); var t2 = tasks.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); bit2.Toggle(); tsk.Delay(48); tsk.TaskContinue(loop); }, "Task2"); var ca = tasks.ContinuousActivate(tasks.AlwaysOn, t1); tasks.ActivateNext(ca, tasks.AlwaysOn, t2); ca.Dispose(); m.EnableInterrupt(); tasks.Loop(); Console.WriteLine(AVRASM.Text(m)); } } }

В этой задаче мы настраиваем нулевой и первый контакты порта B как контакты и меняем значение с 0 на 1 и обратно с периодом 32 мс для нуля и 48 мс для первого контакта.

За управление каждым портом отвечает отдельная задача.

Первое, на что следует обратить внимание, — это определение экземпляра Parallel. Этот класс является ядром управления задачами.

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

Далее идет выделение памяти для хранения данных потока.

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

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

Далее в коде мы описываем набор задач, предназначенных для выполнения под управлением ядра.

Стоит обратить внимание на асинхронную функцию Delay, которую мы используем для генерации задержки.

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

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

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

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

Основанием для вызова задачи является активация сигнала, назначенного потоку задачи.

Сигнал может быть активирован либо программно, либо аппаратно посредством прерываний от периферийных устройств.

Вызов задачи сбрасывает сигнал.

Исключением является предопределенный сигнал AlwaysOn, который всегда активен.

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

Функция LOOP необходима для вызова основного цикла выполнения.

К сожалению, размер выходного кода при использовании Parallel уже становится существенно больше, чем в предыдущих примерах (около 600 команд) и не может быть полностью представлен в статье.

И на десерт — что-то более похожее на живой проект, а именно цифровой термометр.

Все как всегда просто.

Цифровой датчик с интерфейсом SPI, 7-сегментным 4-значным дисплеем и несколькими потоками обработки, обеспечивающими бесперебойную работу.

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

буфер.

Сама программа выглядит так.

Пример 5. Исходный код

using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var led7s = new Led_7(); led7s.SegPort = m.PortC; led7s.Activate(); m.PortD.Direction(0xFF); m.PortD.Activate(); m.PortB[0].

Mode = ePinMode.OUT; var tc77 = new TC77(); tc77.CS = m.PortB[0]; tc77.Port = m.SPI; m.Timer0.Clock = eTimerClockSource.CLK64; m.Timer0.Mode = eWaveFormMode.Normal; var reader = m.DREG("Temperature"); var bcdRes = m.DREG("digits"); var tmp = m.BYTE(); var bcd = new BCD(reader, bcdRes); m.subroutines.Add(bcd); var os = new Parallel(m, 4); os.Heap = new StaticHeap(os, 64); var tmrSig = os.AddSignal(m.Timer0.OVF_Handler); var spiSig = os.AddSignal(m.SPI.Handler, () => { m.SPI.Read(m.TempL); m.TempL.MStore(tmp); }); var actuator = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); tc77.ReadTemperatureAsync(); tsk.Delay(16); tsk.TaskContinue(loop); }, "actuator"); var treader = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); tc77.ReadTemperatureCallback(os, reader, tmp); reader >>= 7; m.CALL(bcd); tsk.TaskContinue(loop); }, "reader"); var display = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); m.PortD.Write(0xFE); m.TempQL.Load(bcdRes.Low); m.TempQL &= 0x0F; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xFD); m.TempQL.Load(bcdRes.Low); m.TempQL >>= 4; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xFB); m.TempQL.Load(bcdRes.High); m.TempQL &= 0x0F; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xF7); m.TempQL.Load(bcdRes.High); m.TempQL >>= 4; led7s.Show(m.TempQL); os.AWAIT(); tsk.TaskContinue(loop); }, "display"); var ct = os.ContinuousActivate(os.AlwaysOn, actuator); os.ActivateNext(ct, spiSig, treader); os.ActivateNext(ct, tmrSig, display); tc77.Activate(); m.Timer0.Activate(); m.EnableInterrupt(); os.Loop(); Console.WriteLine(AVRASM.Text(m)); } } }

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

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

Теги: #Программирование микроконтроллеров #Интернет вещей #arduino #компиляторы #avr #Mega328

Вместе с данным постом часто просматривают: