第2部分。入门→
AVR微控制器的汇编代码生成器库
第1部分。初次认识
下午好,亲爱的哈布罗维特。 我想提请您注意下一个(从大量可用的项目中)对AVR系列流行的微控制器进行编程的项目。
可能会花费大量文本来解释为什么这样做是必要的,但是,仅看示例它与其他解决方案有何不同的示例。 并且,在必要时,将在解析示例的过程中对所有与现有编程系统的解释和比较。 该库现在正在最终确定中,因此某些功能的实现似乎并不是完全最佳的。 同样,在此版本中分配给程序员的某些任务应该进一步优化或自动化。
因此,让我们开始吧。 我想马上澄清一下,所提供的材料绝不应该被视为完整的描述,而只是作为已开发库的某些功能的演示,以帮助理解这种方法可能对读者有多有趣。
我们将不会偏离既定的惯例,而是以一个经典的例子开始,一种微控制器的“ Hello world”。 即,我们使连接到处理器脚之一的LED闪烁。 让我们打开Microsoft的VisualStudio(任何版本都可以)并为C#创建一个控制台应用程序。 对于那些不知道的人,完全可以工作的Community Edition是完全免费的。
实际上,文本本身如下:
源代码示例1using 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宏库的任何环境(该宏库也是所提供编程系统的组件之一,并且可以与NanoRTOSLib结合使用),则可以在仿真器或真实芯片上编译并检查该程序,确保一切正常。
更详细地考虑程序的源代码。 首先,我们将使用的晶体类型分配给变量m。 接下来,为晶体的端口B的零位设置数字输出模式,并激活该端口。 下一行看起来有些奇怪,但是其含义非常简单。 在其中,我们表示要组织一个无限循环,在该循环中,我们将端口B零位的值更改为相反的值。 程序的最后一行实际上是可视化以前以汇编代码形式编写的所有内容的结果。 一切都非常简单和紧凑。 结果几乎与汇编器中的结果没有区别。 输出代码只能有两个问题:第一个-为什么我们仍然不使用它,为什么要初始化堆栈,以及哪种xjmp ? 第一个问题的答案是,同时解释了为什么显示汇编程序而不是最终的十六进制的原因如下:汇编程序形式的结果使您可以进一步分析和优化程序,从而使程序员可以选择和修改他不喜欢的代码片段。 堆栈的初始化至少是出于以下原因:如果不使用堆栈,您将无法提供很多程序。 但是,如果您不喜欢它,请随时对其进行清理。 为此目的,输出到汇编器。 对于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位和1个16位。 建筑师非常努力地在这些计时器中投入尽可能多的功能,因此其设置非常繁琐。
首先,我们计算哪个计数器应使用0.5秒的延迟时间。 如果我们采用16 MHz的晶振时钟频率,即使使用最大的外设分频器,也无法保持在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而不是一个最大值的审议器,因为当我们为500ms的所需时钟频率选择一个分频器1024时,我们将获得一个小数。
我们程序产生的汇编代码将如下所示:
示例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,反之亦然,零为32ms,第一输出为48ms。 单独的任务负责管理每个端口。 首先要注意的是Parallel实例的定义。 此类是任务管理的核心。 在其构造函数中,我们确定同时运行的线程的最大允许数量。 以下是用于存储数据流的内存分配。 该示例中使用的StaticHeap类为每个流分配固定数量的字节。 为了解决我们的问题,这是可以接受的,并且与动态相比,使用固定的内存分配简化了算法,并使代码更紧凑,更快。 在代码的进一步部分,我们描述了一组旨在在内核控制下运行的任务。 您应该注意异步函数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行的源代码和少于1kb的输出代码是一个很好的结果。
在接下来的文章中,我计划在对该项目感兴趣的情况下更详细地介绍使用该库进行编程的原理和功能。