AVR微控制器的汇编代码生成器库。 第4部分

←第三部分。间接寻址和流控制
第5部分。设计多线程应用程序。


AVR微控制器的汇编代码生成器库


第4部分。外设编程和中断处理


在这篇文章的这一部分,我们如所承诺的,将处理微控制器编程中最流行的方面之一,即与外围设备一起工作。 外围编程有两种最常用的方法。 首先,编程系统对外围设备一无所知,仅提供访问设备控制端口的方式。 这种方法实际上与在汇编程序级别上使用设备没有什么不同,并且需要彻底研究与特定外围设备的操作相关的所有端口的用途。 为了方便程序员的工作,有一些特殊的程序,但是它们的帮助通常以生成一系列初始设备初始化结束。 这种方法的优点是可以完全访问所有外围功能,而缺点是编程的复杂性和大量的程序代码。


第二个-与外围设备一起工作是在虚拟设备级别上进行的。 这种方法的主要优点是设备管理简单,并且无需研究特定的硬件实现即可使用它们。 这种方法的缺点是受仿真虚拟设备的目的和功能限制了外围设备的功能。


NanoRTOS库实现了第三种方法。 每个外围设备均由专门的类别进行描述,其目的是简化设备的设置和操作,同时保持其全部功能。 最好通过示例来演示此方法的功能,因此让我们开始吧。


让我们从最简单,最常见的外围设备开始-数字输入/输出端口。 该端口最多可组合8个通道,每个通道均可独立配置为输入或输出。 澄清为8表示控制器体系结构意味着可以为各个端口位分配替代功能,这将其用作水/输出端口,从而减少了可用位的数量。 设置和进一步的工作既可以在单独的位上进行,也可以在整个端口上进行(用一个命令写入和读取所有8位)。 示例中使用的Mega328控制器具有3个端口:B,C和D。在初始状态下,从库的角度来看,所有端口的放电均为中性。 这意味着要激活它们,必须指出其使用方式。 如果尝试访问未激活的端口,程序将生成编译错误。 这样做是为了消除分配替代功能时可能发生的冲突。 要将端口切换到输入/输出模式,请使用“ 模式”命令设置单个位模式,使用“ 方向”通过一个命令设置所有端口位的模式。 从编程的角度来看,所有端口都相同,并且它们的行为由一个类描述。


var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT;// 0   B    m.PortC.Direction(0xFF);//       m.PortB.Activate(); //   m.PortC.Activate(); //  C //   m.PortB[0].Set(); //  0   B  1 m.PortB[0].Clear();//  0   B  0 m.PortB[0].Toggle();//  0   B   m.PortC.Write(0b11000000);// 6  7        var rr = m.REG(); //     rr.Load(0xC0); m.PortC.Write(rr);//      rr var t = AVRASM.Text(m); 

上面的示例演示了如何组织通过端口输出的数据。 总体而言,此处使用端口B进行的工作仅在一个级别上进行,而使用端口C的工作则在整个级别进行。 请注意Activate()激活命令。 其目的是根据先前设置的属性在输出代码中生成一系列设备初始化命令。 因此, Activate()命令始终使用执行时当前的一组设置参数。 考虑从端口读取数据的示例。


  m.PortB.Activate(); //  B m.PortC.Activate(); //  C Bit dd = m.BIT(); //     Register rr = m.REG(); //     m.PortB[0].Read(dd); //  0   B m.PortC.Read(rr);//      rr var t = AVRASM.Text(m); 

在此示例中,出现了新的Bit数据类型。 在高级语言中,此类最接近的类似物是bool类型。 Bit数据类型仅用于存储信息的一位,并允许其值用作分支操作中的条件。 为了节省内存,在存储过程中将位变量组合成多个块,以便使用一个RON寄存器存储8个Bit类型的变量。 除了所描述的类型外,该库还包含另外两种位数据类型: Pin ,具有与Bit相同的功能,但是使用IO和Mbit寄存器将位变量存储在RAM存储器中。 让我们看看如何使用位变量来组织分支


 m.IF(m.PortB[0], () => AVRASM.Comment(",   = 1")); var b = m.BIT(); b.Set(); m.IF(b, () => AVRASM.Comment(",   b ")); 

第一行检查输入端口的状态,如果输入1,则执行条件块的代码。 最后一行包含一个示例,其中将Bit类型的变量用作分支条件。


下一个常见且经常使用的外围设备可以视为硬件计数器/计时器。 在AVR微控制器中,该器件具有大量功能,根据设置,可用于产生延迟,产生具有可编程频率的曲折,测量外部信号的频率以及用作多模PWM调制器。 与I / O端口不同,每个Mega328定时器都有一套独特的功能。 因此,每个计时器由一个单独的类描述。


让我们更详细地考虑它们。 作为每个计时器的信号源,可以同时使用处理器的外部信号和内部时钟。 微控制器的硬件设置允许您配置使用外围设备的全频率,或者通过8开启所有外围设备的单个分离器。由于微控制器允许在较宽的频率范围内运行,因此,为内部时钟期间所需的延迟正确计算定时器分频器值需要处理器频率和预分频器模式。 因此,计时器设置部分采用以下形式


 var m = new Mega328(); m.FCLK = 16000000; //   m.CKDIV8 = false; //     //    Timer1 m.Timer1.Clock = eTimerClockSource.CLK256; //   m.Timer1.OCRA = (ushort)((0.5 * m.FCLK) / 256); //    A m.Timer1.Mode = eWaveFormMode.CTC_OCRA; //    m.Timer1.Activate(); //    Timer1 

显然,设置计时器需要研究制造商的文档,以选择正确的模式并了解各种设置的目的,但是使用该库可以更轻松,更易理解设备,同时保留使用所有设备模式的能力。


现在,我建议您对使用特定设备的描述有所注意,并在继续之前讨论异步操作的问题。 外围设备的主要优点是它们能够在不使用CPU资源的情况下执行某些功能。 由于外围设备操作期间发生的事件相对于CPU中的代码执行流而言是异步的,因此在组织程序与设备之间的交互时可能会出现复杂性。 同步交互方法(其中的程序包含等待所需设备状态的循环)使几乎独立设备的外围设备的所有优势均无效。 更有效和首选的是中断模式。 在这种模式下,处理器将连续执行主线程的代码,并在事件发生时将执行线程切换到其处理程序。 在处理结束时,控制权返回到主线程。 这种方法的优点是显而易见的,但是其使用可能因设置的复杂性而变得复杂。 在汇编器中,要使用中断,您必须:


  • 在中断表中设置正确的地址,
  • 配置设备本身以使其能够正常工作,
  • 描述中断处理功能
  • 提供所有使用过的寄存器和标志的保留,以便中断不会影响主线程的进度
  • 启用全局中断。

为了简化通过中断进行的工作编程,库外围设备描述类包含事件处理程序的属性。 同时,要通过中断组织与外围设备的工作,您只需要描述用于处理所需事件的代码,库将自行执行所有其他设置。 让我们返回计时器设置,并在达到用于比较计时器比较通道的阈值时补充应该执行的代码的定义。 假设我们希望在触发比较通道的阈值时,溢出时会重置I / O端口的某些位。 换句话说,我们希望使用计时器来实现在选定的任意端口上生成PWM信号的功能,其占空比由第一通道的OCRA值和第二通道的OCRB值确定。 让我们看看这种情况下的代码外观。


 var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var bit1 = m.PortB[0]; bit1.Mode = ePinMode.OUT; var bit2 = m.PortB[1]; bit2.Mode = ePinMode.OUT; m.PortB.Activate(); //  0  1   B   //     m.Timer0.Clock = eTimerClockSource.CLK; m.Timer0.OCRA = 50; m.Timer0.OCRB = 170; m.Timer0.Mode = eWaveFormMode.PWMPC_TOP8; //   m.Timer0.OnCompareA = () => bit1.Set(); m.Timer0.OnCompareB = () =>bit2.Set(); m.Timer0.OnOverflow = () => m.PortB.Write(0); m.Timer0.Activate(); m.EnableInterrupt(); //  //   m.LOOP(m.TempH, (r, l) => m.GO(l), (r) => { }); 

关于定时器模式设置的部分已在前面进行了讨论,因此让我们立即进入中断处理程序。 在该示例中,使用三个处理程序使用一个定时器来实现两个PWM通道。 处理程序的代码非常明显,但是可能会出现一个问题,即如何实现前面提到的状态保存,以便中断调用不会影响主线程的逻辑。 保存所有寄存器和标志的解决方案看上去很多余,因此该库分析了中断中资源的使用情况,仅保存了必要的最小值。 空的主循环证实了这样的思想,即连续生成多个PWM信号的任务无需主程序即可工作。


应该注意的是,该库实现了一种统一的方法来处理所有外围设备描述类的中断。 这简化了编程并减少了错误。


我们将继续研究有关中断的工作,并考虑一种情况,在这种情况下,单击连接到输入端口的按钮应导致程序部分执行某些操作。 在我们正在考虑的处理器中,有两种方法可以在输入端口的状态更改时生成中断。 最先进的是使用外部中断模式。 在这种情况下,我们能够为每个结论生成单独的中断,并仅针对特定事件(前沿,衰退,水平)配置反应。 不幸的是,在我们的水晶中只有两个。 另一种方法允许您通过中断来控制输入端口的任何位,但是由于以下事实:当任何已配置位的输入信号发生更改时,事件会在端口级别发生,因此处理更加复杂,并且应通过软件在算法级别上进一步说明中断原因。


作为说明,我们将尝试解决使用两个按钮控制端口输出状态的问题。 其中一个应将我们指示的端口的值设置为1,另一个应重置。 由于只有两个按钮,我们将利用这个机会使用外部中断。


  var m = new Mega328(); m.PortD[0].Mode = ePinMode.OUT; m.PortD.Write(0x0C); // pull-up   m.INT0.Mode = eExtIntMode.Falling; //  INT0  . m.INT0.OnChange = () => m.PortD[0].Set(); //      1 m.INT1.Mode = eExtIntMode.Falling; //  INT1  . m.INT1.OnChange = () => m.PortD[0].Clear(); //     //  m.INT0.Activate(); m.INT1.Activate(); m.PortD.Activate(); m.EnableInterrupt(); //   //  m.LOOP(m.TempL, (r, l) => m.GO(l), (r, l) => { }); 

使用外部中断使我们能够尽可能简单明了地解决我们的问题。


以编程方式管理外部端口不是唯一可行的方法。 特别地,计时器具有允许它们直接控制微控制器的输出的设置。 为此,必须在计时器设置中指定输出控制模式


 m.Timer0.CompareModeA = eCompareMatchMode.Set; 

激活计时器后,端口D的第6位将接收替代功能,并由计时器控制。 因此,我们仅使用软件来设置信号参数,就能够在处理器输出上纯粹在硬件级别上生成PWM信号。 同时,如果尝试使用库工具将忙碌端口转换为输入/输出端口,则在编译级别会出现错误。


我们将在本文的这一部分中看到的最后一个设备是USART串行端口。 该设备的功能非常广泛,但到目前为止,我们仅涉及该设备最常见的一种使用情况。


该端口最流行的用例是将串行终端连接到输入/输出文本信息。 在这种情况下,有关端口设置的代码部分可能如下所示


 m.FCLK = 16000000; //   m.CKDIV8 = false; //     m.Usart.Mode = eUartMode.UART; //    UART m.Usart.Baudrate = 9600; //   9600  m.Usart.FrameFormat = eUartFrame.U8N1; //   8N1 

指定的设置与库中USART的默认设置一致,因此可以在程序文本中部分或全部跳过它们。


考虑一个小示例,在该示例中,我们向终端输出静态文本。 为了不使代码膨胀,我们将自己限制为经典“ Hello world!”终端的输出。 在程序开始时。


  var m = new Mega328(); var ptr = m.ROMPTR(); //      m.CKDIV8 = false; m.FCLK = 16000000; //      m.Usart.Mode = eUartMode.UART; m.Usart.Baudrate = 9600; m.Usart.FrameFormat = eUartFrame.U8N1; //         m.Usart.OnTransmitComplete = () => { ptr.MLoadInc(m.TempL); m.IF(m.TempL!=0,()=>m.Usart.Transmit(m.TempL)); }; m.Usart.Activate(); m.EnableInterrupt(); //   var str = Const.String("Hello world!"); //   ptr.Load(str); //     ptr.MloadInc(m.TempL); //    m.Usart.Transmit(m.TempL); //   . m.LOOP(m.TempL, (r, l) => m.GO(l), (r,l) => { }); 

在此程序中,从新开始,声明常量字符串str 。 该库将所有常量变量放置在程序存储器中,因此,要使用它们,必须使用ROMPtr指针。 输出到终端的数据从字符串序列的第一个字符的输出开始,此后,控制立即进入主循环,而无需等待输出结束。 字节传输过程的完成导致一个中断,该中断在处理程序中读取该行的下一个字符。 如果字符不等于0(库使用零终止格式存储字符串),则将该字符发送到串行接口端口。 如果到达行尾,则不会将字符发送到端口,并且发送周期结束。


这种方法的缺点是固定的中断处理算法。 除了输出静态字符串外,它将不允许以任何其他方式使用串行端口。 该实施方式的另一个缺点是缺乏用于监视端口占用的机制。 如果尝试顺序发送多条线路,则可能会中断前几条线路的传输或混合线路。


解决此问题和其他问题以及与其他外围设备一起使用的更有效方法,将在下一部分中看到。 在其中,我们将仔细研究使用特殊的并行任务管理类进行编程。

Source: https://habr.com/ru/post/zh-CN463627/


All Articles