Хабрахабр

[Из песочницы] Библиотека генератора ассеблерного кода для микроконтроллеров AVR. Часть 1

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

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

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

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

А именно помигаем светодиодом, подключенным к одной из ножек процессора. Не будем отступать от сложившийся практики и начнем с классического примера, своеобразного «Hello world» для микроконтроллеров. Тем, кто не в курсе — достаточный для работы Community Edition абсолютно бесплатен. Откроем VisualStudio от Microsoft (подойдет любой выпуск) и создадим консольное приложение для C#.

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

Исходный код примера 1

using NanoRTOSLib;
using System; namespace ConsoleApp
); 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 (макробиблиотека так же является одной из составных элементов представляемой системы программирования и работает совместно с NanoRTOSLib), то эту программу можно скомпилировать и проверить на эмуляторе или реальном кристалле и убедиться что все работает.

Первым делом назначаем переменной m тип используемого кристалла. Рассмотрим подробнее исходный код программы. Следующая строка выглядит немного странно, но ее смысл совсем простой. Далее устанавливаем для нулевого бита порта B кристалла режим цифрового выхода и активируем порт. Последняя строка программы собственно и визуализирует результат всего написанного ранее в виде ассемблерного кода. В ней мы говорим, что хотим организовать бесконечный цикл в теле которого мы меняем значение нулевого бита порта B на противоположное. А результат практически не отличается от того, что можно было бы написать на ассемблере. Все предельно просто и компактно. Ответом на первый вопрос и одновременно объяснением почему выводится ассемблер, а не готовый HEX будет следующий: результат в виде ассемблера позволяет дополнительно анализировать и оптимизировать программу, позволяя программисту выделять и изменять фрагменты кода, которые ему не нравятся. Вопросов к выходному коду может возникнуть только два: первый — зачем инициализировать стек, если его все равно не используем и что за xjmp? Впрочем, если не нравится — смело убирайте. А инициализация стека оставлена хотя бы из тех соображений, что без использования стека можно придумать не так много программ. Что касается xjmp — это пример использования макросов для увеличения читабельности выходного ассемблера. Вывод в ассемблер для этого и предназначен. Конкретно xjmp — замена для jmp и rjmp с правильной подстановкой в зависимости от длины перехода.

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

Исходный код примера 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(watchdog timer). Очевидно, что программа похожа на предыдущую, поэтому рассмотрим только то, что изменилось. Все, что нужно для его использования — это установить требуемую периодичность, путем установки делителя через свойство WDT. Для работы с большими и не требующими особой точности задержками это наилучший вариант. OnTimeout. Clock и определить действия, которые необходимо выполнить в момент срабатывания события, путем определения кода через свойство WDT. А вот основной цикл можно заменить пустышкой. Так как для работы нам нужны прерывания, их необходимо разрешить командой 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

Здесь мы использовали следующую логику — если код не используется — код не нужен. У тех, кто знаком с этим процессором безусловно возникнет вопрос куда делись еще несколько векторов прерываний. Поэтому рассмотрим еще один способ, а заодно и посмотрим, как в библиотеке организована работа с таймерами. Поэтому таблица прерываний и заканчивается на последнем используемом векторе.
Несмотря на то, что программа прекрасно справляется с поставленной задачей, самым придирчивым может не понравится то, что набор возможных задержек ограничен, а шаг слишком грубый. 2 8-битных и один 16-битный. В кристалле Mega328, который взят за образец, их целых 3 штуки. Архитекторы очень постарались вложить в эти таймеры как можно больше возможностей, поэтому и их настройка достаточно объемна.

Если взять тактовую частоту кристалла 16 Мгц, то даже с максимальным предделителем периферии не получается уложится в 8-разрядный счетчик. Сначала посчитаем какой счетчик следует применить для нашей задержки в 0,5 сек. Поэтому не будем усложнять и воспользуемся единственным доступным для нас 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 для требуемой тактовой частоты в 500ms, которую мы хотим получить, получается дробное число.

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

Результат компиляции примера 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

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

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

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

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

Исходный код примера 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)); } }
}

За управление каждым из портов отвечает отдельная задача. В этой задаче мы настраиваем нулевой и первый выводы порта В на вывод и меняем значение с 0 на 1 и обратно с периодом 32ms для нулевого и 48ms для первого вывода. Этот класс представляет из себя ядро управления задачами. Первое, на что следует обратить внимание, это определение экземпляра Parallel. Далее следует выделение памяти для хранения данных потоков. В его конструкторе мы определяем максимально допустимое количество одновременно работающих потоков. Для решения нашей задачи это допустимо, а использование фиксированного распределения памяти по сравнению с динамическим упрощает алгоритмы и делает код более компактным и быстрым. Использованный в примере класс StaticHeap выделяет под каждый поток фиксированное количество байт. Следует обратить внимание на асинхронную функцию Delay, которую мы используем для формирования задержки. Далее в коде мы описываем набор задач, которые предназначены для запуска под управлением ядра. По истечении установленного интервала ядро возвращает управление задаче с команды, следующей за командой Delay. Ее особенность состоит в том, что при вызове этой функции в настройках потока устанавливается требуемая задержка, а управление передается в ядро. В нашем случае обе задачи настроены на выполнение в бесконечном цикле с возвратом управления ядру в конце каждого цикла. Другой особенностью задачи является программирование поведения потока задачи по ее завершению в последней команде задачи. В случае необходимости, завершение задачи может освобождать поток или передавать его для выполнения другой задачи.

Сигнал может активироваться как программным путем, так и аппаратно по прерываниям от периферийных устройств. Основанием для вызова задачи является активация сигнала, назначенного потоку задачи. Исключением является предопределенный сигнал AlwaysOn, который всегда находится в активном состоянии. Вызов задачи сбрасывает сигнал. Функция LOOP необходима для вызова основного цикла исполнения. Это дает возможность создавать задачи, которые будут получать управление в каждом цикле опроса. К сожалению размер выходного кода при использовании Parallel уже становится существенно больше чем в предыдущих примерах (примерно 600 команд) и не может быть целиком приведен в статье.

Все как всегда просто. И на сладкое — нечто более похожее на живой проект, а именно цифровой термометр. В одном гоняем цикл для динамической индикации, в другом события по которым запускается цикл считывания температуры, в третьем читаем принятые c датчика значения и преобразуем из бинарного кода в BCD а затем в сегментный код для буфера динамической индикации. Цифровой датчик с интерфейсом SPI, 7-сегментный 4-х разрядный индикатор и несколько потоков обработки для того, чтобы все крутилось.

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

Исходный код примера 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)); } }
}

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть