←第4部分。对外围设备进行编程和处理中断
AVR微控制器的汇编代码生成器库
第5部分。设计多线程应用程序
在本文的前面部分中,我们详细介绍了使用该库进行编程的基础。 在上一部分中,我们熟悉了中断的实现以及使用它们时可能出现的限制。 在本文的这一部分中,我们将介绍使用Parallel类对并行进程进行编程的一种可能的选择。 使用此类可以简化应用程序的创建,在该应用程序中,应在几个独立的程序流中处理数据。
用于单核系统的所有多任务系统彼此相似。 多线程是通过分派器的工作来实现的,分派器为每个线程分配一个时隙,并在完成时控制并向下一个线程提供控制。 各种实现之间的区别仅在于细节,因此我们将主要针对此实现的特定功能进行详细介绍。
线程中进程执行的单位是任务。 系统中可以存在无限数量的任务,但是在任何给定时间,只能激活它们中的一定数量,这受调度程序中工作流数量的限制。 在此实现中,工作流的数量在管理器构造函数中指定,并且无法随后更改。 在此过程中,线程可以执行任务或保持空闲状态。 与其他解决方案不同, Parallel Manager不会切换任务。 为了使任务将控制权返回给调度程序,必须在其代码中插入适当的命令。 因此,任务中时隙持续时间的责任在于程序员,如果任务花费的时间太长,程序员必须在代码的某些位置插入中断命令,并在任务完成时确定线程的行为。 这种方法的优点是程序员可以控制任务之间的切换点,这使您可以在切换任务时极大地优化保存/恢复代码,并且可以消除与线程安全数据访问有关的大多数问题。
为了控制正在运行的任务的执行,使用了特殊的Signal类。 该信号是位变量,其设置用作启动流中的任务的启用信号。 信号值可以手动设置,也可以通过与此信号相关的事件设置。
当任务由调度员激活或可以通过编程方式执行时,该信号将重置。
系统中的任务可以处于以下状态:
已停用 -所有任务的初始状态。 任务不会占用流程,并且不会转移执行控制。 完成命令后,将返回此状态以激活任务。
已激活 - 激活后任务所在的状态。 激活过程将任务与执行线程和激活信号相关联。 如果任务信号被激活,管理器将轮询线程并启动任务。
已阻止 -激活任务时,可能已经将一个信号作为信号分配给了该信号,该信号已经用于控制另一个线程。 在这种情况下,为了避免程序行为的歧义,激活的任务将进入锁定状态。 在这种状态下,任务占用线程,但是即使激活了其信号,也无法接收控制。 完成任务或更改激活信号后,调度程序将检查并更改线程中任务的状态。 如果线程阻塞了任务,且信号与释放的任务匹配,则激活第一个找到的任务。 如有必要,程序员可以根据程序所需的逻辑独立锁定和解锁任务。
等待 -执行Delay命令后任务所处的状态。 在这种状态下,直到经过所需的时间间隔,任务才可以接收控制。 在Parallel类中,使用16 ms WDT中断来控制延迟,从而不占用系统所需的定时器。 如果您需要以较小的间隔获得更高的稳定性或分辨率,而不是使用“ 延迟”,则可以使用计时器信号进行激活。 应该记住的是,延迟精度仍然很低,并且会在“调度程序响应时间”-“系统中的最大时隙持续时间+调度程序响应时间”的范围内波动。 对于具有精确时间范围的任务,应使用混合模式,其中并行类中未使用的计时器独立于任务流工作,并在纯中断模式下处理间隔。
在线程中执行的每个任务都是一个独立的进程。 这就需要定义两种类型的数据:流的本地数据(仅在此流的框架内应可见并可以更改),以及用于在流之间交换和访问共享资源的全局数据。 在此实现的框架中,全局数据是由先前在设备级别考虑的命令创建的。 要创建局部任务变量,必须使用任务类中的方法创建它们。 本地任务变量的行为如下:当在将控制权转移到调度程序之前任务被中断时,所有本地寄存器变量都存储在流的内存中。 返回控制后,将在执行下一条命令之前恢复本地寄存器变量。
具有与Parallel类的Heap属性关联的IHeap接口的类负责存储流的本地数据。 此类的最简单实现是StaticHeap ,它为每个线程实现相同内存块的静态分配。 如果任务根据对本地数据量的需求而分散很大,则可以使用DynamicHeap ,它使您可以分别确定每个任务的本地内存大小。 显然,在这种情况下使用流内存的开销将明显更高。
现在,我们以两个流为例仔细研究类语法,每个流独立地切换一个单独的端口输出。
var m = new Mega328 { FCLK = 16000000, CKDIV8 = false }; m.PortB.Direction(0x07); var bit1 = m.PortB[1]; var bit2 = m.PortB[2]; m.PortB.Activate(); var tasks = new Parallel(m, 2); tasks.Heap = new StaticHeap(tasks, 16); 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();
该程序的顶行已经为您所熟悉。 在它们中,我们确定控制器的类型,并将端口B的第一和第二位分配为输出。 接下来是Parallel类的变量的初始化,其中在第二个参数中,我们确定最大执行线程数。 在下一行中,我们分配内存以容纳局部变量流。 我们有相等的任务,所以我们使用StaticHeap 。 下一个代码块是任务定义。 在其中,我们定义了两个几乎相同的任务。 唯一的区别是控制端口和延迟量。 要使用本地任务对象,将指向本地任务tsk的指针传递到任务代码块 。 任务文本本身非常简单:
- 创建本地标签以组织无限的切换周期
- 端口状态反转
- 控制权返回给调度程序,任务进入等待状态指定的毫秒数
- 返回指针设置为该块的起始块,控制权返回给调度程序。
显然,在一个具体示例中,最后一个命令可以用普通命令代替,以转到块的开头,并且在该示例中仅出于演示目的而给出。 如果需要,可以通过复制任务和增加线程数来轻松地扩展示例以控制大量结论。
任务中止命令以将控制权转移到调度程序的完整列表如下
AWAIT(信号) -流将所有变量保存在流的内存中,并将控制权转移给调度程序。 下次激活流时,将从AWAIT之后的下一条指令开始,恢复变量并继续执行。 该命令旨在将任务划分为多个时隙,并根据信号Signal→Processing 1→Signal→Processing 2等方案实现状态机。
AWAIT命令可以将信号作为可选参数。 如果参数为空,则保存激活信号。 如果在参数中指定,则在激活指定信号时将进行所有后续任务调用,并且与前一个信号的通信将丢失。
TaskContinue(标签,信号) -该命令终止流并将控制权交给调度程序,而不保存变量。 下次激活流时,控制权将转移到标签label 。 可选的Signal参数允许您覆盖下一个呼叫的流激活信号。 如果未指定,则信号保持不变。 未指定信号的命令可用于组织单个任务中的周期,其中每个周期在单独的时隙中执行。 完成前一个任务后,它还可用于将新任务分配给当前线程。 与周期相比,此方法的优势释放线程→突出显示流是一种更高效的程序。 使用TaskContinue消除了管理器在池中搜索空闲线程的需要,并保证了在没有空闲线程的情况下尝试分配线程时的错误。
TaskEnd() -任务完成后清除流。 任务结束,线程被释放,可用于通过Activate命令分配新任务。
延迟(ms) -与使用AWAIT一样 ,流将所有变量保存在流的内存中,并将控制权转移给调度程序。 在这种情况下,以毫秒为单位的延迟值记录在流头中。 在分派器循环中,如果延迟字段中的值非零,则不会激活该流。 通过每16 ms中断WDT定时器来更改所有流的delay字段中的值。 当达到零值时,将取消执行禁止,并设置流激活信号。 标头中仅存储了一个单字节的延迟值,这提供了相对窄的可能延迟范围,因此,为了实现更长的延迟, Delay()使用局部流变量创建了一个内部循环。
本示例中命令的激活是使用ContinuousActivate和ActivateNext命令执行的。 这是启动时激活初始任务的一种特殊方式。 在初始激活阶段,我们保证没有单个繁忙的线程,因此激活过程不需要预先搜索任务的空闲线程,而是可以按顺序激活任务。 ContinuousActivate在零线程中激活任务,并返回指向下一个线程头的指针,而ActivateNext函数使用该指针在顺序线程中激活以下任务。
该示例使用AlwaysOn信号作为激活信号。 这是系统信号之一。 其目的是将始终执行任务,因为这是始终激活且不会因使用而重置的唯一信号。
该示例以循环调用结束。 此函数启动调度程序的周期,因此此命令应为代码中的最后一个命令。
考虑另一个示例,其中使用库可以大大简化代码的结构。 假设它是一个条件控制设备,它注册一个模拟信号并将其以十六进制代码的形式发送到终端。
var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var cData = m.DREG(); var outDigit = m.ARRAY(4); var chex = Const.String("0123456789ABCDEF"); m.ADC.Clock = eADCPrescaler.S64; m.ADC.ADCReserved = 0x01; m.ADC.Source = eASource.ADC0; m.Usart.Baudrate = 9600; m.Usart.FrameFormat = eUartFrame.U8N1; var os = new Parallel(m, 4); os.Heap = new StaticHeap(os, 8); var ADS = os.AddSignal(m.ADC.Handler, () => m.ADC.Data(cData)); var trm = os.AddSignal(m.Usart.TXC_Handler); var starts = os.AddLocker(); os.PrepareSignals(); var t0 = os.CreateTask((tsk) => { m.LOOP(m.TempL, (r, l) => m.GO(l), (r, l) => { m.ADC.ConvertAsync(); tsk.Delay(500); }); }, "activate"); var t1 = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); var mref = m.ROMPTR(); mref.Load(chex); m.TempL.Load(cData.High); m.TempL >>= 4; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[0]); mref.Load(chex); m.TempL.Load(cData.High); m.TempL &= 0x0F; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[1]); mref.Load(chex); m.TempL.Load(cData.Low); m.TempL >>= 4; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[2]); mref.Load(chex); m.TempL.Load(cData.Low); m.TempL &= 0x0F; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[3]); starts.Set(); tsk.TaskContinue(loop); }); var t2 = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); trm.Clear(); m.TempL.Load('0'); m.Usart.Transmit(m.TempL); tsk.AWAIT(trm); m.TempL.Load('x'); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[0]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[1]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[2]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[3]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.Load(13); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.Load(10); m.Usart.Transmit(m.TempL); tsk.TaskContinue(loop, starts); }); var p = os.ContinuousActivate(os.AlwaysOn, t0); os.ActivateNext(p, ADS, t1); os.ActivateNext(p, starts, t2); m.ADC.Activate(); m.Usart.Activate(); m.EnableInterrupt(); os.Loop();
这并不是说我们在这里看到了很多新东西,但是您可以在这段代码中看到一些有趣的东西。
在该示例中,首先提到了ADC(模数转换器)。 该外围设备旨在将输入信号的电压转换为数字代码。 转换周期由ConvertAsync函数启动,该函数仅启动进程而无需等待结果。 转换完成后,ADC产生一个中断,以激活adcSig信号。 请注意adcSig信号的定义。 除中断指针外,它还包含一个代码块,用于存储ADC数据寄存器中的值。 最好在中断发生后立即执行的所有代码(例如,从设备寄存器中读取数据)应位于此位置。
转换任务是将二进制电压代码转换为条件终端的四字符十六进制表示形式。 在这里,我们可以注意到使用函数来描述重复的片段以减小源代码的大小,并使用常量字符串进行数据转换。
从实现字符串的格式化输出的角度来看, 传输问题很有趣,在该字符串中,静态和动态数据的输出被组合在一起。 该机制本身不能被认为是理想的;相反,它证明了管理处理程序的可能性。 在这里,您还可以注意执行期间重新定义激活信号,这会将激活信号从ConvS更改为TxS ,反之亦然。
为了更好地理解,我们用文字描述了程序的算法。
在初始状态下,我们启动了三个任务。 它们中的两个具有无效信号,因为用于转换任务的信号(adcSig)在模拟信号的读取周期结束时被激活,而用于传输任务的ConvS被尚未执行的代码激活。 因此,启动后要启动的第一个任务始终是测量。 该任务的代码开始ADC转换周期,此后500 ms任务进入等待周期。 在转换周期结束时, adcSig标志被激活 ,从而触发转换任务。 在该任务中,实现了将接收到的数据转换为字符串的循环。 在退出任务之前,我们激活ConvS标志,以明确我们有新数据要发送到终端。 exit命令将返回点重置为任务的开始,并将控制权交给调度程序。 通过设置ConvS标志,可以将控制权转移到传输任务。 传输序列的第一个字节后,任务中的激活信号变为TxS 。 结果,在字节传输完成后,将再次调用传输任务,这将导致下一个字节的传输。 传输序列的最后一个字节后,任务将返回ConvS激活信号 ,并将返回点重置为任务的开始。 循环完成。 当测量任务完成等待并激活下一个测量周期时,下一个周期将开始。
在几乎所有的多任务系统中,都有用于线程之间交互的队列的概念。 我们已经发现,由于在此系统中任务之间的切换是一个完全受控的过程,因此使用全局变量在任务之间交换数据是完全可能的。 但是,在许多任务中使用队列是合理的。 因此,我们不会抛开这个话题,而是看看它如何在库中实现。
要在程序中实现队列,最好使用RingBuff类。 顾名思义,该类使用写入和提取命令实现了环形缓冲区。 读写数据是通过“ 读写”命令执行的。 读写命令没有参数。 缓冲区使用构造函数中指定的寄存器变量作为数据源/接收器。 可通过参数IOReg类访问此变量。 缓冲区的状态由两个标志Ovf和Empty确定,这两个标志有助于确定写入期间的溢出状态和读取期间的溢出状态。 此外,该类还可以确定在溢出/溢出事件上运行的代码。 RingBuff对Parallel类没有依赖关系,可以单独使用。 使用该类时的限制是允许的容量,出于代码优化的原因,该容量应为2的幂(8.16.32等)的倍数。
下面给出了使用该类的示例。
var m = new Mega328(); var io = m.REG();
本部分总结了库功能。 不幸的是,关于库的功能还有很多方面,甚至没有提到。 将来,如果对该项目感兴趣,则会计划使用该库来专门解决特定问题的文章,并对需要单独出版的复杂问题进行更详细的描述。