通过赛普拉斯UDB微控制器PSoC控制RGB LED



引言


我一直想学习在赛普拉斯PSoC控制器中对UDB块进行编程的技术,但是某种程度上我的手并没有伸手。 因此,出现了一个可以解决的问题。 了解了网络中的资料后,我意识到与UDB一起使用的实际建议仅限于计数器和PWM的各种变化。 由于某些原因,所有作者都对这两个规范示例进行了更改,因此其他内容的描述可能会让读者感兴趣。

这样啊 动态管理一长排WS2812B RGB LED时出现问题。 对此问题的经典方法是已知的。 您可以使用普通的Arduino,但是输出会以编程方式进行,因此在输出数据时,其他所有内容均处于空闲状态,否则时序图将失败。 您可以采用STM32并通过DMA至PWM或通过DMA至SPI输出数据。 技术是已知的。 我什至一次通过SPI亲自控制了16条二极管。 但是开销很大。 对于PWM的情况,LED中的一个数据位在存储器中占据8位,对于SPI,则为3至4位(取决于控制器中的PLL冷却度)。 虽然LED很少,但这并不令人恐惧,但是如果有几百个,那么200 * 24 = 4800位= 600字节的有用数据应物理存储在大于4 KB的PWM缓冲区或大于2 KB的SPI-缓冲区中。选项。 为了动态指示缓冲区,应该有多个缓冲区,并且STM32F103具有RAM,可存储大约20 KB的所有内容。 并不是说我们已经完成了一项无法实现的任务,而是一个相当重要的理由,那就是检查是否可以在PSoC上实现这一点而无需花费额外的RAM。

理论参考


首先,让我们找出UDB这样的野兽,以及它们是如何工作的。 控制器制造商的精彩教学影片将对此有所帮助。

您应该从这里开始观看,然后在每个视频的结尾都有一个指向下一个系列的链接。 您将逐步了解基础知识,并考虑规范的示例“计数器”。 好吧,还有一个交通信号灯控制系统。

大致相同,但切成小块,您可以在这里看到 。 我的视频无法播放,但可以在本地下载和观看。 除其他外,还有一个典型的PWM实施示例。

成品解决方案


为了避免重新发明轮子(反之亦然-从其他人的经验中学习方法),我在网上搜索了用于控制RGB LED的现成解决方案。 最受欢迎的解决方案是StripLightLib.cylib。 但是多年来,他已经计划增加Add DMA支持。 但是我想尝试一种不依赖中央处理器的解决方案。 我想开始该过程,而忽略它,集中精力准备下一个框架。

https://github.com/PolyVinalDistillate/PSoC_DMA_NeoPixel上找到了符合我期望的解决方案。

一切都在UDB上实现(但是LED只是一个借口,目的是学习UDB)。 支持DMA。 那里的项目显然组织得很漂亮。

选择解决方案的问题作为基础


PSoC_DMA_NeoPixel项目中“固件”的排列方式,希望阅读本文的人可以看到。 这将修复材料。 现在,我只想说我首先简化了原始固件的逻辑,却没有减少消耗的资源(但是,它变得更容易理解)。 然后,他开始尝试替换自动机逻辑,这有望获得资源,但遇到了严重的问题。 因此,他决定-这并没有消除! 含糊的疑问开始折磨我:英语作者也有同样的问题吗? 他的演示通过LED闪烁得非常漂亮。 但是,如果我们用“所有单元”代替漂亮的填充物,而不是用示波器而是用示波器控制输出,该怎么办?
因此,我们尽可能粗略地(您甚至可以说“蛮横”)形成数据:

memset (pPixelArray,0xff,sizeof(pPixelArray)); //Call NeoPixel update function (non blocking) to trigger DMA pixel update NP_Update(); 

在这里,我们在示波器上看到这样的图片:



第一位的宽度与其余位不同。 我要求发送所有单位,但不是全部请假。 其中清零! 更改扫描:



每八位的宽度是不同的。

通常,此示例作为独立的解决方案不适合,但可以作为灵感的来源-只是完美的。 首先,用眼睛看不见它的不可操作性(LED仍然亮着,眼睛看不到它们以一半的最大亮度发光),但是代码结构合理,可以很好地将其作为基础。 第二,该示例为寻找简化方法提供了空间,第三,它使您思考如何修复缺陷。 最重要的是要了解装备! 因此,再次阅读本文后,我建议尝试解析原始示例,以了解其工作原理。

实践部分


现在我们开始练习。 我们正在测试为UDB开发固件的主要方面。 考虑一下关系和基本技术。 为此,请打开我的项目版本 。 左边的块存储有关工作文件的信息。 默认情况下,“ 源”选项卡处于打开状态。 该项目的主要来源是main.c文件。 实际上,“ 源文件”组中没有其他工作文件。



Generated Source组包含库函数。 最好不要编辑它们。 UDB的“固件”每次更改后,将重新生成该组。 那么,在这个田园诗中,UDB的代码描述在哪里? 要查看它,您需要切换到“ 组件”选项卡:



原始项目的作者制作了两级组件。 NeoPixel_v1_2.cysch电路位于顶层 。 从主要方案中可以看出:



组件如下:



稍后将讨论该方案的软件支持。 同时,发现它本身是一个常规的DMA单元和一个特定的符号NeoPixDrv_v1 。 上面的树中描述了这个神秘的块,该树来自以下工具提示:



“固件” UDB


打开该组件(扩展名为.cyudb的文件)。 打开的图纸非常巨大。 我们开始了解什么。



与原始项目的作者不同,我认为数据的每个传输都以三个相等的(及时的)部分的形式进行:

  1. 起始部分(总是1)
  2. 数据部分
  3. 停止部分(始终为0)

使用这种方法,不需要大量的计数器(在最初,计数器多达三个,这消耗了大量的资源)。 所有部分的持续时间相同,可以使用一个寄存器进行设置。 因此,固件的过渡图包含以下状态:

空闲状态。 机器保留在其中,直到新数据到达FIFO。



从培训视频中,我还不清楚机器的状态与ALU有何关系。 作者当然会使用交流,但作为初学者,我无法立即看到交流。 让我们快速详细了解一下。 上图显示, 空闲状态编码为值1'b0。 3'b000会更正确,但编辑器将重做所有内容。 数据路径块的输入描述如下:



如果双击它们,将显示更详细的版本:



这意味着ALU指令的地址的零位对应于设置机器状态的变量的零位。 第一个是第一,第二个是第二个。 如果需要,可以将任何变量甚至表达式与ALU指令的地址位进行匹配(在原始版本中,ALU指令的地址的第二位由一个表达式进行匹配,此外,当前版本中未明确使用它,但是很明显它是一个易于理解的示例,您可以看一下)。

这样啊 在输入的当前设置(即机器的二进制状态代码)下,使用了这样的ALU指令。 当我们处于代码为000的空闲状态时,将使用null指令。 这是:



我已经从该条目中知道这是一个普通的NOP。 但是您可以双击它并阅读完整版本:



NOP随处可见。 寄存器中没有任何内容。

现在,让我们找出什么样的神秘标记!NoData ,迫使机器离开空闲状态。 这是数据路径块的出口。 总共最多可以描述六个出口。 只是Datapath可以产生更多的标志,但是没有足够的跟踪资源供所有人使用,因此我们需要选择真正需要的六个(或更少)。 这是图中的列表:



如果双击它,详细信息将显示出来:



这是可以显示的标志的完整列表:



选择所需的标志后,应为其命名。 从现在开始,系统将带有一个标志。 如您所见, NoData标志是链F0块状态(空)的名称 。 也就是说,一个迹象表明输入缓冲区中没有数据。 Ah !NoData分别对其进行反转。 数据可用性的迹象。 一旦数据进入FIFO(以编程方式或使用DMA),该标志将被清除(其反转被锁定),并且在下一个时钟周期,自动机将退出空闲状态并进入GetData状态。



如您所见,自动机刚进入一个时钟周期就将无条件地退出此状态。 在此状态的过渡图上未指示任何动作。 但是您应该始终看看ALU会做什么。 状态码为1'b1,即3'b001。 我们看一下ALU中的对应地址:



有事 没有阅读本文的经验,请双击相应的单元格将其打开:



因此,ALU本身仍然不执行任何操作。 但是FIFO0的内容,即来自程序或DMA块的数据,将被放置在A0寄存器中。 展望未来,我将说A0用作移位寄存器,字节将从中以串行形式退出。 寄存器A1将放置寄存器D1的值。 通常,所有D寄存器通常在硬件开始激活之前就已填充到软件中。 然后,在检查API时,我们将看到时钟滴答数已放入此寄存器,该寄存器设置了第三位的持续时间。 这样啊 在A0中,移位值下降,而在A1中,位开始部分的持续时间。 并且在下一拍时,机器肯定会进入状态Constant1



就像状态的名称所暗示的,常数1是在这里生成的,让我们看一下LED的文档。 这是单位的传输方式:



这是-零:



我添加了红线。 如果我们假设三分之二的持续时间相等,则满足脉冲持续时间的要求(在同一文档中给出)。 即,任何脉冲都由起始单元,数据位和停止零组成。 实际上,当机器处于Constant1状态时,将传输启动单元。

在此状态下,机器将单元锁定在其内部触发器中。 触发器的名称为CurrentBit 。 在原始项目中,通常是一个触发器来设置辅助自动机的状态。 我认为那台机器只会让所有人感到困惑,所以我才开始触发。 在任何地方都没有描述。 但是,如果输入状态属性,则表中将显示以下记录:



在图的状态下有这样的文本:



请勿惊叹等于符号。 这些是编辑器的功能。 在生成的Verilog代码(由同一系统自动生成)中,将出现一个箭头:

 Constant1 : begin CurrentBit <= (1); if (( CycleTimeout ) == 1'b1) begin MainState <= Setup1 ; end end 

此触发器中锁存的值是整个块的输出:



也就是说,当机器进入Constant1状态时,我们正在开发的块的输出将变为1。 现在,让我们看看如何为地址3'b010编程ALU:



我们揭示了这个元素:



从寄存器A1中减去单元1。 ALU的输出值落入寄存器A1中。 上面,我们认为A1是用于设置输出脉冲持续时间的时钟计数器。 让我提醒您,它是在最后一步从D1启动的。
退出状态的条件是什么? CycleTimeOut 。 在输出中对其进行了如下描述:



因此,我们将逻辑结合在一起。 在先前状态下,先前由程序填充的寄存器D1的内容落入寄存器A1中。 在此步骤中,机器将CurrentBit触发器转换为1,在ALU中,A1寄存器在每个时钟周期减少。 当A1变为零时,该标志将自动升起,作者将其命名为CycleTimeout ,其结果是机器将切换到Setup1状态。

状态Setup1准备用于发送有用脉冲的数据。



我们看一下3'b011处的ALU指令。 我将立即打开它:



似乎ALU没有任何动作。 NOP操作。 而且ALU输出无法到达任何地方。 但是事实并非如此。 一个非常重要的动作是ALU中的数据转移。 事实是,输出中的进位位已连接到我们的ShiftOut链:



由于进行了这种移位操作,移位后的值本身将不会到达任何地方,但是ShiftOut链将获取寄存器A0的最高有效位的值。 即,应该发送的数据。 在图的状态下,可以看出该值( 已将 ALU留在ShiftOut链中)锁存到CurrentBit触发器中。 让我再次显示绘图,以免使文章倒退:



该位的第二部分的传输开始-立即数为0或1。

我们返回有关ALU的说明。 除了已经说过的以外,很明显寄存器D1的内容将再次放入寄存器A1中,以便再次测量脉冲的后三分之一的持续时间。

DataStage状态与Constant1状态非常相似。 自动机仅从A1中减去一个,并在到达零时进入下一个状态。 让我什至显示如下:



像这样:



然后是Setup2的状态,我们已经知道其本质。



在这种状态下, CurrentBit触发器重置为零(因为将发送脉冲的三分之一,停止部分,并且始终为零)。 ALU将D1的内容加载到A1中。 您甚至可以用训练有素的眼睛简短地看到它:



Constant0的状态与Constant1DataStage的状态完全相同。 从A1减去单位。 当值达到零时,退出ShiftData状态:





ShiftData的状态更为复杂。 在ALU的相应说明中,执行以下操作:



寄存器A0移位1位,结果存回A0。 在A1中,再次放置D1的内容,以便开始测量下一个数据位的起始第三个。

最好考虑优先级的输出箭头,为此我们双击ShiftData状态。



如果没有传输最后一位(关于该标志的形成方式,稍低一点),那么我们为当前字节的下一位传输一位。

如果最后一位被发送并且FIFO中没有数据,我们将进入空闲状态。

最后,如果最后一位被发送,但FIFO中有数据,我们转到下一个字节的选择和发送。

现在介绍位计数器。 ALU中只有两块电池:A0和A1。 它们已分别被移位寄存器和延迟计数器占用。 因此,外部使用位计数器。



双击它:



引导时的值为六。 它使用变量部分中描述的LoadCounter标志加载:



就是说,当获取下一个数据字节时,此常量将一直加载。

当机器进入ShiftData状态时,计数器会减小该值。 当它达到零时,输出TerminalCount被连接,并连接到我们的FinalBit种子的电路。 正是此电路设置了机器是传输当前字节的下一个位还是传输新字节(嗯,还是等待新的数据包)。

实际上,一切都来自逻辑。 如何生成SpaceForData信号(设置饥饿输出的状态(通知DMA单元可以传输下一个数据)),请读者独立跟踪。

软件支援


原始项目的作者选择在描述集成解决方案的模块中为整个系统提供软件支持。 让我提醒您,我们正在谈论这个障碍:



从这个级别上,可以控制DMA库单元和UDB部分中包括的所有部分。 为了实现该API,原始作者添加了头文件和程序文件:



这些文件的正文格式使您感到难过。 整个责任归咎于PSoC Designer开发人员对“纯”产品的热爱。 因此,可怕的宏和公里名称。 C ++中的类组织将在这里派上用场。 至少在实施RTOS MAX时我们进行了检查:美观而方便。 但是在这里您可以争论很多,但是您将不得不使用从上而下降低给我们的东西。 我将仅简要展示包含这些宏的API函数的样子:

 volatile void* `$INSTANCE_NAME`_Start(unsigned int nNumberOfNeopixels, void* pBuffer, double fSpeedMHz) { //work out cycles required at specified clock speed... `$INSTANCE_NAME`_g_pFrameBuffer = NULL; if((0.3/(1.0/(fSpeedMHz))) > 255) return NULL; unsigned char fCyclesOn = (unsigned char)(0.35/(1.0/(fSpeedMHz))); `$INSTANCE_NAME`_g_nFrameBufferSize = nNumberOfNeopixels*3; //Configure for 19.2 MHz operation `$INSTANCE_NAME`_Neo_BITCNT_Start(); //Counts bits in a byte //Sets bitrate frequency in number of clocks. Must be larger than largest of above two counter periods CY_SET_REG8(`$INSTANCE_NAME`_Neo_DPTH_D1_PTR, fCyclesOn+1); //Setup a DMA channel `$INSTANCE_NAME`_g_nDMA_Chan = `$INSTANCE_NAME`_DMA_DmaInitialize(`$INSTANCE_NAME`_DMA_BYTES_PER_BURST, `$INSTANCE_NAME`_DMA_REQUEST_PER_BURST, HI16(`$INSTANCE_NAME`_DMA_SRC_BASE), HI16(`$INSTANCE_NAME`_DMA_DST_BASE)); if(pBuffer == NULL) ... 

这些游戏规则必须被接受。 现在,您知道了在开发功能时从何处汲取灵感(最好在原始项目中进行此操作)。 我更喜欢谈论细节,采用生成器已经处理过的选项。

生成代码(如下所述)后,该文件将存储在此处:



并且该视图将已经完全可读。 到目前为止,有两个功能。 第一个初始化系统,第二个开始从缓冲区到LED线的数据传输。

初始化会影响系统的所有部分。 UDB系统的一部分是七位计数器的初始化:

  NP_Neo_BITCNT_Start(); //Counts bits in a byte 

有一个常数计算应加载到D1寄存器中(我记得它设置了每个第三位的持续时间):

 unsigned char fCyclesOn = (unsigned char)(0.35/(1.0/(fSpeedMHz))); CY_SET_REG8(NP_Neo_DPTH_D1_PTR, fCyclesOn+1); 

设置DMA模块将占用大部分功能。 缓冲区用作源,UDB块的FIFO0用作接收器(公里记录中的NP_Neo_DPTH_F0_PTR)。 作者在数据传输功能中拥有此设置的一部分。 但是,我认为,为每次传输而进行所有计算都是很浪费的。 尤其是当您认为函数内部的动作之一看起来非常庞大时。

 //work out cycles required at specified clock speed... NP_g_pFrameBuffer = NULL; NP_g_nFrameBufferSize = nNumberOfNeopixels*3; //Setup a DMA channel NP_g_nDMA_Chan = NP_DMA_DmaInitialize(NP_DMA_BYTES_PER_BURST, NP_DMA_REQUEST_PER_BURST, HI16(NP_DMA_SRC_BASE), HI16(NP_DMA_DST_BASE)); ... NP_g_nDMA_TD = CyDmaTdAllocate(); CyDmaTdSetConfiguration(NP_g_nDMA_TD, NP_g_nFrameBufferSize, CY_DMA_DISABLE_TD, TD_INC_SRC_ADR | TD_AUTO_EXEC_NEXT); CyDmaTdSetAddress(NP_g_nDMA_TD, LO16((uint32)NP_g_pFrameBuffer), LO16((uint32)NP_Neo_DPTH_F0_PTR)); CyDmaChSetInitialTd(NP_g_nDMA_Chan, NP_g_nDMA_TD); 

在第一个功能的背景下,第二个功能是简洁主义。 只是在性能要求完全免费的初始化阶段才调用第一个。 在操作过程中,最好不要将处理器周期浪费在任何多余的事情上:

 void NP_Update() { if(NP_g_pFrameBuffer) { CyDmaChEnable(NP_g_nDMA_Chan, 1); } } 

显然没有足够的功能来与多个缓冲区一起使用(以提供双缓冲),但是总的来说,对API功能的讨论不在本文讨论范围之内。 现在最主要的是展示如何为开发的固件添加软件支持。 现在我们知道该怎么做了。

项目生成


那么,整个固件部分已经准备就绪,添加了API,下一步该怎么做? 选择菜单项Build-> Generate Application



如果一切顺利,则可以打开“ 结果”选项卡并查看带有rpt扩展名的文件。



它显示了固件实施中投入了多少系统资源。





当我将结果与原始项目中的结果进行比较时,我的灵魂变得更加温暖。

现在转到“ 源”选项卡,并开始使用软件部分。 但这已经是微不足道的,不需要特殊说明。



结论


我希望从这个例子中,读者可以学到一些有关UDB块的实际工作的新知识。 我试图专注于特定任务(LED控制)以及设计方法,因为我必须理解专家们显而易见的某些方面。 在尝试的回忆鲜活的时候,我试图标记它们。 至于已解决的问题,对我来说,时序图并不像原始开发的作者那样理想,但它们完全适合LED文档中定义的公差,并且系统资源大大减少了。

实际上,这只是发现的非标准信息的一部分。 特别是,从大多数资料来看,UDB似乎仅适用于串行数据,但事实并非如此。 找到了应用笔记,其中简要介绍了如何驱动和并行处理数据。 我们可以根据该信息考虑具体示例(尽管不可能使FX2LP黯然失色,但赛普拉斯的另一款控制器:PSoC具有较低的USB总线速度)。

我的脑子里一直在想着如何解决“闪烁” 3D打印机的问题,这使我很痛苦。 在那里,为步进电机提供服务的中断只会消耗CPU时间的疯狂百分比。 通常,我在有关RTOS MAX文章中谈到了很多有关中断和处理器时间的内容 。 据估计,为维修步进电动机,有可能将所有临时小屋完全带到UDB,从而使处理器仅承担计算任务,而不必担心他没有时间在专用时隙内执行此操作。

但是只有当主题很有趣时,才可以对这些事情进行推理。

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


All Articles