- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
。
如果你开启了广告屏蔽,请将博客园加入白名单,帮助博客园渡过难关,谢谢! 。
。
在21年做物理实验和23年客串电赛之后,我带着STM32重回电子DIY界。这次的项目是一个电池供电的补光灯,由于用途更偏向艺术创作而非严肃照明,选用了WS2812 RGB灯带;控制灯带的参数需要呈现给用户,通过LCD屏的方式;还有些其他的外设,包括输入等,不是重点。RGB灯带和LCD屏都通过SPI总线与STM32G431CBU6单片机连接.
WS2812使用800 kbps的单线归零码控制,它不是单片机具有的任何一种硬件总线。在800 kbps的速率下,直接用GPIO驱动显然只能是阻塞的。熟悉WS2812协议的读者可能会疑惑它与SPI的关系,详见下文分解.
LCD方面,此前我用的也一直是阻塞式的传输,因为在10M的速率下传输一两个字节用中断反而不合算(DMA结束也会进中断),而阻止我将若干序列打包到一起进行一次DMA传输的,是LCD信号中的 D/ C ,其高低电平代表数据与指令,往往是一个指令字节后跟两个数据字节,诸如此类。如果能用某种机制自动控制 D/ C 线,那么就很容易把指令、数据打包用DMA发送了.
理论与实践均表明,这两个外设的阻塞时间都达到了毫秒的量级,因此有必要研究非阻塞的、占用CPU时间少的控制方法。我们定义标题中的“全自动”为:在启动一次传输后,无需CPU任何干预,单片机内部其他组件能自动完成这次传输,结束后产生中断,并且中断引入的额外指令(push、pop寄存器等)的执行时间远小于实际传输的时间,从而CPU只有很小一部分时间在执行ISR。为了达到这一比例,这种传输的粒度对于LCD而言,至少要达到一次传输完成一个字符(5*7像素)的刷新,能做到字符串当然更好;对于RGB而言,没有别的选择,一次传输更新所有灯的亮度.
我们假设读者掌握基本的电子技术,具有对定时器和SPI总线的认识,且使用过中断与DMA。虽然达到这一程度的读者很大概率已经使用过本文关注的两种器件,但是为了文章的完整性,我们仍会提供对器件的基本介绍.
。
接单片机用的LCD,一般有8080并口、三线SPI和四线SPI等连接方式。我们不考虑读取,因为很多应用场景下从来不会从LCD读取。向LCD写入,无论是哪种接口,都需要指定当前写的字或字节是数据(data)还是指令(command),通过 D/ C 信号或编码在串行数据中的 D/ C 位.
并口一般接在单片机类似于FSMC的控制器上,并映射到32位寻址空间中,而后可以像变量一样进行读写:
#define ST7735S_CMD *(volatile uint8_t*)(0x60000000)
#define ST7735S_DAT *(volatile uint8_t*)(0x60040000)
ST7735S_CMD = 0x36; // MADCTL
ST7735S_DAT = 0b11000000;
D/ C 线一般接到FSMC的一根地址线上,从而该位为高的地址对应数据,为低的对应指令.
三线SPI下,一次传输的长度为9 bit,其中首位为 D/ C 位,随后从MSB到LSB。通常单片机都是支持9位格式的,无论是标准的SPI还是USART组件的SPI模式.
四线SPI下,SPI部分非常标准,但是 D/ C 信号是标准SPI以外的,一般是用GPIO来控制。 D/ C 信号无需一直保持所需的电平,它跟LSB一起被采样.
。
LCD的控制并不是无脑地全屏填数据。在初始化完成以后,基本上只会用到3个指令:
CASET
(0x2A),设置列范围
RASET (0x2B),设置行范围 。
RAMWR (0x2C),往所设定范围内填充像素(按行或按列,由 MADCTL 事先设定),一个像素两个字节 。
例:向(16, 32)像素写入白色(0xFFFF),传输序列为 。
D/ C |
数据 | 释义 |
---|---|---|
L | 0x2A | CASET |
H | 0x00, 0x10, 0x00, 0x10 | x=16~16 |
L | 0x2B | RASET |
H | 0x00, 0x20, 0x00, 0x20 | y=32~32 |
L | 0x2C | RAMWR |
H | 0xFF, 0xFF | 白色 |
由此可见,单独填充一个像素需要13字节,效率很低(顺便吐槽,Adafruit的LCD库画字符真的就是一个像素一个像素画的)。要提高效率,可以在本地渲染好一片区域,然后集中发送过去,这样可以降低指令的占比.
读者自然会问,这么说来,为什么不在本地把全屏都渲染好一次发送过去呢?以128*160的屏为例,在RGB565模式下,需要40 KB显存,而我用的G431一共只有32 KB内存。另一方面,128*160的分辨率对于1.8寸的屏来说像素密度已经偏低了,而屏幕更小的话又看不见了,唉……总之分块刷新是必须的.
在三种连接方式中,最容易做非阻塞的是三线SPI——只需把 D/ C 位打包进指令或数据即可。但是这样做的代价是DMA缓冲区大小翻倍,让本不富裕的RAM雪上加霜.
并口的速度可以达到10M量级,比两种SPI快很多,但比170M的CPU慢很多。做阻塞式传输一般都是可以接受的,至少比阻塞式SPI更能接受。但是话说回来,由于指令和数据是两个不同的地址,并口也很难做非阻塞式的传输.
四线SPI的空间利用率和并口一样都是100%,而权衡的难点已经在前言中提到了——阻塞式有一点点慢,非阻塞式粒度太小。接下来我们讨论全自动接管 D/ C 线的方法,从而可以增大四线SPI非阻塞式传输的粒度.
。
以前用8位机的时候,一个SPI、一个UART,不够用;后来用了32位机,两个SPI、两个UART,还是不够用;从来不会不够用的是动辄十几个的定时器。不仅如此,随手抓个引脚,它应该有一半的概率连接到了某个定时器的某个通道(未经验证,瞎猜的)。刚好,我用的核心板上的LCD的 D/ C 信号就可以接到定时器上.
定时器工作在一种类似于PFM的模式中。初始时 CNT=1 , ARR 设置到一定值, CCRx=0 。定时器启动后, CNT 慢慢累加到 ARR ,然后变成 0 ,此时 CNT==CCRx ,产生compare match:
这使Channel x的输出翻转; 。
产生DMA请求,从内存中读一个值写入 ARR .
这个功能非常基本,但凡是个有输出通道的定时器都能轻松胜任,不需要更高级的特性.
经试验,SPI的 SCK 频率是严格的CPU整数分频,不会在字节间插入额外的延迟(归功于FIFO;仅适用于STM32G4系列,其他系列可能具有不同的SPI IP)。因此,要被写入 ARR 的值为 N*8-1 , N 为连续的指令或数据的字节数.
。
实验平台为STM32G431核心板(某宝有售),预留了0.96或1.14寸LCD的排线焊盘,买来对应的LCD焊上即可。单片机与LCD的连接如下:
LCD引脚 | 单片机引脚 | Alternate function |
---|---|---|
SCL |
PB13 |
SPI2_SCK |
SDA |
PB15 |
SPI2_MOSI |
RS (D/ C) |
PB10 |
TIM2_CH3 |
CS |
PB12 |
N/A |
Cube配置:
SPI2 |
TIM2 |
---|---|
SPI方面,我们需要一个传输完成的中断来拉高 CS 信号。SPI本身只提供了FIFO有空位的中断,我们转换一下思路,SPI接收一定是完成以后才会触发中断的。因此我们再开一路DMA给 SPI_RX ,以它的中断为传输完成的标志。当然,接收的数据没有意义,那就设置一个字节的垃圾桶,DMA接收的内容往里面写,内存地址不增长.
定时器方面,由于时间不紧张以及为了省事更省心,我们关闭了 ARR 的preload.
一些注意事项:
HAL配置很耗时,会破坏SPI与定时器同步的初始条件。解决方案有:a) 自己调用LL;b) 把HAL代码拷出来,去掉最后启动SPI或定时器的一行,两个分别配置完后一起启动,只相差一行代码的时间.
一次传输结束后, D/ C 一般为高,而下次传输一般是以 D/ C 为低开始的。要手动指定这一信号的状态,可以调用 LL_TIM_OC_SetMode(htim2.Instance, LL_TIM_CHANNEL_CH3, LL_TIM_OCMODE_FORCED_INACTIVE) ,完事后记得改回去.
注意设置初始状态,以及定时器的第一个字节是不走DMA的,而是直接设置给 ARR ,并且加上一个偏置以补偿初始时 CNT=1 以及SPI启动的延迟,经试验 htim->Instance->ARR = *pData + 3 比较合适.
。
为了方便用逻辑分析仪查看结果,系统时钟设置为16 MHz。SPI时钟频率为4 MHz.
传输起始:
中间切换指令/数据类型:
在最后一段数据较短时, D/ C 信号会产生一次额外的翻转,不过是在传输结束之后,没有实际影响.
。
由于涉及两个组件的协同工作,并且它们都是由CPU启动的,就必然会有它们之间同步问题的担忧,这包括初始的相位差和两者频率差异导致的累积相位差。在SPI FIFO始终不空的前提下,SPI的时序精确可知;在定时器DMA延迟足够低的前提下,定时器的时序也精确可知,可以做到两者频率严格一致。至于初始相位差,一定程度上是试出来的,并且还依赖于各个分频系数,甚至编译器优化,当然也与选用的单片机系列有关。好在这一延迟只需精确到SPI传输一个字节的时间,算是相对宽松的要求.
实际应用中有时SPI还从USART的SPI模式中来,需要注意USART的分频比SPI组件的复杂很多,还需要考虑起始、终止位等等.
前面的实验都是在4分频下进行的,即SPI时钟频率和定时器频率均为CPU频率的四分之一。经尝试,更低的频率都能正常工作,但是若将频率提升到二分频,定时器不能按照预期工作。观察波形并分析原因,应该是更新 ARR 时 CNT 已经超过 ARR 导致的,这也是定时器需要引入preload的原因,前面我们图省事把它关掉了。一般来说,32位机的SPI不大会工作在这么高的频率;如果需要,必须开启 ARR preload,并额外考虑初始和终止处理.
单实现一个非阻塞式的写LCD操作并没有可用的实际意义——也许你要刷新的是全屏,但前面已经提到内存不够放下全屏内容;又或许你先提交一串指令,然后又要更新另一块区域,而要开始这段传输还得等前一段完成,又成阻塞式的了。因此下一步要做的是实现一个队列,在一次DMA结束的中断里启动下一次DMA,以及可选地一个图形管理器,从而可以在队列中存储绘图指令而非渲染后的画面,以节省内存开销。 我的一篇早期文章 可能提供了相关的代码工具.
。
nRF52840的SPI外设 集成了 D/ C 信号,但是它只能指定前多少字节是指令,此后全为数据,不能在一次DMA中重新切换回指令.
一些为低功耗优化的单片机,如STM32U5,具有 带链表功能的DMA 。这提供了非常大的自由:你可以把每一段指令或数据放在一次DMA传输中,中间插入对定时器或GPIO的DMA以改变 D/ C 的电平,最后把这些配置整合成一个链表,DMA控制器会自动遍历链表中指定的操作——无需CPU干预.
。
引用我之前的 文章 :
WS2812B的信号是单线的,一方面这简化了灯带的设计,对级联也比较友好,但另一方面这种信号不是任何一种常见的总线,也不能由常见总线信号通过简单变换得到,这带来了一些困难.
每一位都是先高电平后低电平, 0 和 1 的差别在于高低电平的时间不同, 0 的高电平时间比较短。允许的时间范围都是比较宽的。通常每一位都是等长的,那么一位的时间范围为1.16 μs到1.38 μs.
每个灯有4个引脚: VCC 、 GND 、 DIN 、 DO 。 DO 上的信号是 DIN 信号除了前24个bit以外的部分,这24个bit以绿红蓝、MSB优先的顺序锁存进WS2812B。前一个灯的 DO 接后一个的 DIN ,如此级联.
没有信号时数据线保持低电平,当低电平时间超过280 μs时就会RESET,锁存的数据更新到亮度上。所有级联的灯在几乎同一时刻更新.
友情提醒:各个渠道的灯珠内置的WS2812B不尽相同,有的支持TTL电平而有的必须 VH > 0.7 VDD ,有的时序宽松有的严格,而且你根本找不到所购买的灯珠对应的规格,所以量产前请务必打样测试.
。
GPIO加延时的方案就不讲了。我在网上找到的硬件实现有:
SPI的 MOSI 信号 。
前三种方案都来自于 这篇文章 .
SPI的 MOSI 信号可以直接驱动WS2812。若用3位SPI编码一位WS2812信号, 0 为一个高加两个低, 1 为两个高加一个低;若用4位SPI编码一位WS2812信号, 0 为一个高加三个低, 1 为两个高加两个低.
空间利用率1/3或1/4。3位方案下,运算会相对复杂一些;而4位的方案空间利用率稍低.
定时器PWM 。
这是GitHub上最常见的方案,毕竟如前所述,大家定时器都多到用不完.
定时器工作在PWM模式下,频率固定为800 kHz,占空比为 CCRx / (ARR+1) 。每个计数周期用DMA传送进新的 CCRx ,从而可以逐周期地改变占空比,用以表示WS2812的 0 和 1 .
一般来说, CCRx 用低8位即可,此时空间利用率1/8,即一位WS2812信号需要一个字节内存.
定时器触发的GPIO DMA 。
这是一个略有难度的实现。开一个定时器的两个通道,开三个DMA通道,请求分别为计数器溢出和两个通道的匹配,目标地址为GPIO的寄存器。在溢出时,所有IO变为高电平;需要写 0 的位在先匹配的时候置低,需要写 1 的在另一个匹配置低.
若DMA宽度为16位,则空间利用率为1/32,因为每一个WS2812的bit需要两个16位GPIO写入.
SPI与定时器联合 。
我们让定时器产生两倍于 SCK 频率的方波 WO2 ,上升沿对齐; MOSI 设置为上升沿更新,从 SCK 上升沿到下一个上升沿为一个bit。在这一bit中,高电平占前1/4为WS2812B的 0 ,1/2为 1 .
LUT 寄存器的8位分别存放 IN[2:0] 的8种状态对应的输出。根据前面的时序图,在 011 、 101 和 111 三种情况下输出为 1 , LUT 值为 0xA8 .
空间利用率100%,但是在ATtiny3217上只能单字节传输(人家连DMA都没有).
。
我们发现前3个方案的空间利用率都比较低,在有100个灯有效数据2.4 Kb下,这三个方案需要的DMA缓冲区都比较大(其实并不是不能承受,但我们要面向未来)。GitHub上一般的方案是开一个长为两个灯的环形DMA,在传输完成一半时更新前一半,完成一轮时更新后一半,但这违背了我们“全自动”的目标.
方案4的空间利用率非常理想,但是因为涉及到和定时器的协同,就像前半部分LCD一样,容易出时序问题。尽管定时器波形是由CPU时钟得到,SPI也是,但我们更希望定时器波形能直接从 SCK 获得.
很自然的想法是把 SCK 倍频一下,但是实现起来几乎没有可行性。同样很自然地,转换一下思路,我们就把 SCK 当作原来的倍频信号,而原来 SCK 就用现在 SCK 的二分频取代——二分频是很容易获得的.
很遗憾,这种方案下空间利用率下降到了50%,但这就是本文要介绍的方案。我们将在最后讨论100%空间利用率的衍生方案.
首先对 SCK 进行二分频,使用的是D触发器74LVC1G74:将反向输出 Q 接到输入 D 上,在时钟 CP (接 SCK )的每个上升沿锁存,即输出信号反相,每两个上升沿一个周期,即二分频。将输出 Q 标记为 DIV 信号.
这样一来,每一个WS2812的bit将由两个SPI的bit产生。我们规定WS2812的 0 对应SPI的 00 , 1 对应 11 。事实上第二个bit的值是无所谓的,但是我们将在后面看到这样做的好处.
然后是后面的组合逻辑。根据时序图,我们列出真值表:
MOSI |
DIV |
SCK |
RGB |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 0 | 1 | 0 |
0 | 1 | 0 | 0 |
0 | 1 | 1 | 1 |
1 | 0 | 0 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 0 | 1 |
1 | 1 | 1 | 1 |
用眼睛看也好,画卡诺图也好,反正最后化简出来的逻辑是: \(\mathrm{RGB} = (\mathrm{MOSI} + \mathrm{SCK}) \cdot \mathrm{DIV}\) 。然而一个或门和一个与门并不能一起用一片常用逻辑芯片实现,我们可以把它写成 \(RGB = \overline{ \overline{\mathrm{MOSI} \cdot \mathrm{DIV}} \cdot \overline{\mathrm{SCK} \cdot \mathrm{DIV}}}\) ,这样就能用一片74HC00(四2输入与非门)实现了.
一个小问题是D触发器的初始状态是不确定的,为了使 DIV 信号的闲时电平为低,我们把D触发器的 R D 信号接到单片机一个GPIO,该引脚电平为低时,D触发器的输出 Q 保持低电平。原则上上电后清零一下就可以,保险起见也可以每次传输前清零一次.
。
实验平台仍然是STM32G431核心板.
逻辑电路需要额外搭建。需要的逻辑芯片我手头都有,焊到贴片转直插的板上再在面包板上接接线就可以实验了,但是我觉得这样做不优雅.
终于到了让我夹带私货的时候了。21年的时候我设计了一批贴片万用板,不仅集成了SOP、SOT-23和TSSOP等多种封装(共享焊盘),还引出了一些横向竖向的互连线路,用户只需在合适的位置焊接上电阻、电容,就能实现中等复杂度的电路。对于运放、比较器等特殊的IC,也有专门的PCB可以同时支持同相、反相放大、加减运算、一阶、二阶有源滤波器等等。此外,不仅板间可以在二维地通过排针排母互连,甚至垂直方向上都可以连接.
设计很花哨,但并不是没有实用价值。下面图片展示的是23年电赛期间我们制作的模块,由3层组成:底层是用正电压产生负电压的电源转换模块,中间是模拟信号加法电路,上面是SMA信号输入。这个实现非常优雅,至少我自认为如此.
言归正传,我们延续这种优雅的设计,在一块最大支持TSSOP-28的万用板上搭建所需的逻辑.
焊接前 | 焊接后 | 细节 |
---|---|---|
Cube配置:
时钟选择25 MHz的HSE,最终信号速率781.25 kHz,接近800 kHz.
SPI宽度为16位,对应8位实际数据,其中的对应关系可以一位一位算,更快的方式是查表:
static const uint16_t rgb_table[256] =
{
0x0000, 0x0003, 0x000C, 0x000F, 0x0030, 0x0033, 0x003C, 0x003F,
0x00C0, 0x00C3, 0x00CC, 0x00CF, 0x00F0, 0x00F3, 0x00FC, 0x00FF,
0x0300, 0x0303, 0x030C, 0x030F, 0x0330, 0x0333, 0x033C, 0x033F,
0x03C0, 0x03C3, 0x03CC, 0x03CF, 0x03F0, 0x03F3, 0x03FC, 0x03FF,
0x0C00, 0x0C03, 0x0C0C, 0x0C0F, 0x0C30, 0x0C33, 0x0C3C, 0x0C3F,
0x0CC0, 0x0CC3, 0x0CCC, 0x0CCF, 0x0CF0, 0x0CF3, 0x0CFC, 0x0CFF,
0x0F00, 0x0F03, 0x0F0C, 0x0F0F, 0x0F30, 0x0F33, 0x0F3C, 0x0F3F,
0x0FC0, 0x0FC3, 0x0FCC, 0x0FCF, 0x0FF0, 0x0FF3, 0x0FFC, 0x0FFF,
0x3000, 0x3003, 0x300C, 0x300F, 0x3030, 0x3033, 0x303C, 0x303F,
0x30C0, 0x30C3, 0x30CC, 0x30CF, 0x30F0, 0x30F3, 0x30FC, 0x30FF,
0x3300, 0x3303, 0x330C, 0x330F, 0x3330, 0x3333, 0x333C, 0x333F,
0x33C0, 0x33C3, 0x33CC, 0x33CF, 0x33F0, 0x33F3, 0x33FC, 0x33FF,
0x3C00, 0x3C03, 0x3C0C, 0x3C0F, 0x3C30, 0x3C33, 0x3C3C, 0x3C3F,
0x3CC0, 0x3CC3, 0x3CCC, 0x3CCF, 0x3CF0, 0x3CF3, 0x3CFC, 0x3CFF,
0x3F00, 0x3F03, 0x3F0C, 0x3F0F, 0x3F30, 0x3F33, 0x3F3C, 0x3F3F,
0x3FC0, 0x3FC3, 0x3FCC, 0x3FCF, 0x3FF0, 0x3FF3, 0x3FFC, 0x3FFF,
0xC000, 0xC003, 0xC00C, 0xC00F, 0xC030, 0xC033, 0xC03C, 0xC03F,
0xC0C0, 0xC0C3, 0xC0CC, 0xC0CF, 0xC0F0, 0xC0F3, 0xC0FC, 0xC0FF,
0xC300, 0xC303, 0xC30C, 0xC30F, 0xC330, 0xC333, 0xC33C, 0xC33F,
0xC3C0, 0xC3C3, 0xC3CC, 0xC3CF, 0xC3F0, 0xC3F3, 0xC3FC, 0xC3FF,
0xCC00, 0xCC03, 0xCC0C, 0xCC0F, 0xCC30, 0xCC33, 0xCC3C, 0xCC3F,
0xCCC0, 0xCCC3, 0xCCCC, 0xCCCF, 0xCCF0, 0xCCF3, 0xCCFC, 0xCCFF,
0xCF00, 0xCF03, 0xCF0C, 0xCF0F, 0xCF30, 0xCF33, 0xCF3C, 0xCF3F,
0xCFC0, 0xCFC3, 0xCFCC, 0xCFCF, 0xCFF0, 0xCFF3, 0xCFFC, 0xCFFF,
0xF000, 0xF003, 0xF00C, 0xF00F, 0xF030, 0xF033, 0xF03C, 0xF03F,
0xF0C0, 0xF0C3, 0xF0CC, 0xF0CF, 0xF0F0, 0xF0F3, 0xF0FC, 0xF0FF,
0xF300, 0xF303, 0xF30C, 0xF30F, 0xF330, 0xF333, 0xF33C, 0xF33F,
0xF3C0, 0xF3C3, 0xF3CC, 0xF3CF, 0xF3F0, 0xF3F3, 0xF3FC, 0xF3FF,
0xFC00, 0xFC03, 0xFC0C, 0xFC0F, 0xFC30, 0xFC33, 0xFC3C, 0xFC3F,
0xFCC0, 0xFCC3, 0xFCCC, 0xFCCF, 0xFCF0, 0xFCF3, 0xFCFC, 0xFCFF,
0xFF00, 0xFF03, 0xFF0C, 0xFF0F, 0xFF30, 0xFF33, 0xFF3C, 0xFF3F,
0xFFC0, 0xFFC3, 0xFFCC, 0xFFCF, 0xFFF0, 0xFFF3, 0xFFFC, 0xFFFF,
};
。
RGB灯按照预期点亮.
。
细心的读者可能会发现,上面的时序图中并没有展示原理图中的 RGB_RES 。其实我根本没有接这根线,因此上电后 DIV 的状态也是不确定的。前面的时序图是 DIV 上电后为低电平的结果,但是如果它上电后是高电平,RGB灯也一样工作,看下面的时序图很容易明白这一点.
事实上,输出信号只是滞后了一个 SCK 周期的时间(若SPI的第二个bit不是前一个的复制,则不会有此结果,这就是前面提到的好处)。唯一不同的是, DIV 闲时为高电平,而SPI传输结束后 MOSI 保持最后一个bit的电平,若为高,则输出也一直保持高电平,将导致WS2812收不到 RESET 信号,从而无法更新亮度。为了避免这种现象,只需保证最后一个字节的LSB为 0 ——但这最后一个字节不一定要是实际亮度的最后一个字节,它可以是附加的字节,从而对实际亮度没有任何影响.
如果你执着于这个 RGB_RES 又不想占用GPIO,这里还有两个方案,一个是用电阻电容实现开机清零,另一个如下图(不解释了,自己理解吧):
SPI的分频只有2的幂次,即使考虑了时钟树上前级的分频,这会导致若要CPU运行在最高频率,最终产生的信号频率会偏离800 kbps较远(实际上在一个比较宽的范围内都能工作)。使用USART的SPI模式有助于改善频率问题,但是由于起始位终止位的存在,USART可能会破坏最终产生的信号,我还没有验证过.
以上我们讨论和实现的都是单串WS2812控制信号。如果因为刷新率限制或者为了方便布线需要多路控制信号,这些方案是否能扩展?
基于SPI MOSI 的方案1,自然可以用更多的SPI外设扩展,之间相互独立;或者用QSPI扩展到4路,但是只能同时使用.
基于定时器PWM的方案2,很容易扩展到4路,只需同一定时器的4个通道,而且可以自由配置启动哪些通道(如果启用连续的通道,可以考虑DMA burst);当然也可以加入其他定时器,配置就更自由了.
基于GPIO DMA的方案3,能直接扩展到16路(尽管实际上很少能同时使用一个GPIO port的所有pin),如果真的需要这么多路,个人倾向于此方案,毕竟现在STM32G030的价格真的比你去菜市场买颗白菜还便宜…… 。
本文提出的方案:
可以用更多SPI扩展,通道数即SPI实例数,外部电路数量同样成正比; 。
要扩展到2路,可以将SPI换为I2S,外加一些逻辑让 WS 信号选择当前输出哪一路——两路不能同时输出,这只能优化布线而不能提高吞吐量; 。
要扩展到2路,还可以用SAI(Serial Audio Interface),把它配置为共享时钟的两个SPI,这是能提高吞吐量的; 。
要扩展到4路,可以将SPI换为QSPI(注意STM32G431并没有QSPI),D锁存器只需一个,但与非门共需要9个.
回顾一下前面提到的方案4,ATtiny3217不能实现连续的多字节WS2812信号的根本问题不在于它没有DMA,而是SPI字节之间有额外的CPU周期。在前一板块我们已经验证过,STM32的SPI时钟是非常连续的,那么这套方案能不能用在STM32上呢(别忘了它的空间效率达到了100%!)?难点在于SPI和定时器之间的同步需要精确到单CPU周期,但是我相信借助STM32G系列引入的DMA请求同步系统(在DMAMUX内),这个方案也是有希望实现的.
。
前面提到的ATtiny3217具有内部的可编程逻辑,但这并非8/16位机的专利,目前已经能见到一些32位机搭载了相似的功能,如Silicon Labs的EFM32以及射频SoC EFR32等,具有12异步通道的 PRS (Peripheral Reflex System),每个都能实现2输入组合逻辑,通道之间有互连,可以组合成简单的时序逻辑.
。
当我给别人讲起本文所描述的SPI加定时器或者SPI加外部逻辑这种奇怪的搭配,我常听到两种声音.
第一种是“还能这样用???”的确,像定时器PWM的频率和占空比用DMA改变这种算是相对深入的用法,而总线信号加逻辑运算会要求你回顾尘封的数电知识,都是听起来觉得可能有道理但是要自己想到还是需要很多基础和经验的。最近培训班席卷嵌入式,我想读者们多少有所耳闻。我敢打包票培训班出来的全是这一类.
第二种是“搞这些干什么,上FPGA得了!”我认可这种观点,因为用FPGA实现在除成本以外的任何方面都是更优的,我也为我到现在都只会入门级的FPGA编程而对过去的自己怀恨在心。然而,用FPGA一定比用MCU高级吗?也许是,也许不是。为了省点芯片的成本去花很长时间构思一个精巧的电路值得吗?有时成本更重要,有时你的时间更宝贵。无论如何,构思一个方案,查点相关资料,确认原创性,优雅地实现——我很快乐,这就够了.
。
最后此篇关于利用SPI实现全自动化——LCD屏与RGB灯的文章就讲到这里了,如果你想了解更多关于利用SPI实现全自动化——LCD屏与RGB灯的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
关闭。这个问题需要details or clarity .它目前不接受答案。 想改进这个问题吗? 通过 editing this post 添加细节并澄清问题. 关闭 5 年前。 Improve t
背景 我有一个 Azure Runbook(名为 RunStoredProcedure2),定义如下: param( [parameter(Mandatory=$True)] [string] $Sq
我有一个名为“团队”的表,其中包含“非事件”列,另一个表“事件”,其中包含“时间”列。如果任何团队的“事件”中的最新日期发生在 X 时间之前,如何将“非事件”列更新(为 true)? 我知道这可以通过
下面的问题可能有点令人困惑,但我会尽力以最好的方式解释它。 假设我们为一家制造公司工作。它制造然后用于制造产品的组件。在以下示例中,有 3 个组件和 2 个最终产品。 组件和产品的需求如下: comp
我有代码可以让我在一个范围内选择一个项目: COleVariant vItems = cstrAddr; hr = AutoWrap(
我正在开发一个应用程序,该应用程序有 4 种语言的大约 50 个应用内购买,这给了我很多表单和子表单、框和子框,需要使用 iTunesConnect 的令人痛苦且设计糟糕的表单来填充。 我想知道是否有
我想在 Azure 自动化中使用 powershell 脚本来安排打开/关闭资源。 我想在不创建帐户的情况下执行此操作,因为我们的域强制重置密码。我知道自动化帐户会创建一个证书 - 当使用资源管理器(
我尝试从 azure 自动化 run book power shell 自动检索 azure SQL 数据库中的数据。我发现azure自动化帐户的模块中缺少SQL Server模块。我已经导入了该模块
我正在自动化 Outlook,并且需要控制电子邮件的发件人身份。用户将在 Outlook 中设置两个或多个帐户,我需要能够选择从哪个帐户发送电子邮件。有什么想法吗? 需要 Outlook 2003 及
我尝试从 azure 自动化 run book power shell 自动检索 azure SQL 数据库中的数据。我发现azure自动化帐户的模块中缺少SQL Server模块。我已经导入了该模块
假设我有一个网站,我可以(随时)登录并每隔 x 小时提交数据(单击登录后可见的链接),我将如何自动化此过程? 我构建了一个图形用户界面,它为用户(现在是我,为了我自己的方便)提供了一个包含一些信息的界
我正在开发一个程序,它的任务是我们为它定义一些号码(我们的一些手机号码)并且它应该在 Telegram 中注册它们,然后获取发送到该号码的所有消息。如您所知,在 Telegram 中注册需要提供电话号
关闭。这个问题需要更多focused .它目前不接受答案。 想改进这个问题吗? 更新问题,使其只关注一个问题 editing this post . 关闭 3 年前。 Improve this qu
最近我在处理 CSS Sprite 。一切正常。 我创建了一个 sprite、.css 文件和 html 结构。看起来像 .sprites{ background-image:url
我想为一款名为 Dune 2000 的策略游戏创建一个叠加层。令人讨厌的是,要创建 10 个士兵,每次完成一个都必须单击该图标。没有队列。因此,在不干扰游戏运行方式的情况下,我想听听鼠标移动的声音,当
我是 python 的初学者,我想从自动化开始。以下是我正在尝试执行的任务。 ssh -p 2024 root@10.54.3.32 root@10.54.3.32's password: 我尝试通过
当我将一些 urlencoded Javascript 粘贴到 Firefox 和 Chrome 上的 URL 时,我看到了一些奇怪的事情发生。是否可以使用此技术告诉 Chrome 访问 URL,然后
我需要将大量请求自动提交到基于云的数据库接口(interface) (Intelex)。没有任何方法可以批量提交某些操作,但是提交单个请求所必需的只是让经过身份验证的用户尝试打开 Web 链接。因此,
假设我有一个进程的内存转储。我想对其运行报告,所以基本上我想打开 WinDBG,加载 SOS 并运行一个脚本,该脚本运行一些命令,解析输出,然后基于此运行更多命令。 除了像 SendKeys 这样的
我正在使用 ffmpeg 创建视频剪辑。我想自动化该过程并保存剪辑,而无需手动为要保存的每个文件命名。这是我拥有的代码。它将创建并保存剪辑,但只能使用扩展名和不是名字。 @echo off cd /d
我是一名优秀的程序员,十分优秀!