STM32F103上的3D图形

图片

关于如何使用既没有速度也没有内存的控制器处理不可编辑并显示实时三维图形的简短故事。

回顾2017年(根据文件修改日期),我决定从AVR控制器切换到功能更强大的STM32。自然,第一个控制器是广为人知的F103。拒绝使用现成的调试板来支持按照其要求从头开始生产是很自然的。奇怪的是,几乎没有门框(除了UART1应该带到普通的连接器上,而且不要在接线上扎手)。

与AVR相比,这款产品的特性相当不错:72 MHz的时钟(实际上,您可以超频至100 MHz,甚至更高,但后果自负!),20 kB的RAM和64 kB的闪存。另外,大量的外围设备在使用时,主要的问题就是不必担心这种丰富的情况,并且意识到不需要铲除所有十个寄存器来启动,只要在正确的寄存器中设置三个位就足够了。至少直到你想要一些奇怪的东西。

当拥有这种能力的第一个欣快感过去时,就产生了一种探索其极限的愿望。作为一个有效的示例,我选择了使用所有这些矩阵,照明,多边形模型以及在ili9341控制器上显示320x240的Z缓冲区来计算三维图形的方法。要解决的两个最明显的问题是速度和体积。每种颜色16位的320x240屏幕尺寸提供每帧150 kB。但是我们拥有的总RAM仅为20 kB ...而这150 kB必须每秒至少传输10次,也就是说,交换速率至少应为1.5 MB / s或12 MB / s,这看起来已经是内核上的一大负担。幸运的是,在该控制器中有一个RAP模块(直接内存访问,也称为直接内存访问,DMA),该模块不允许通过从空到空的输血操作来加载内核。也就是说,您可以准备缓冲区,告诉模块“这里有数据缓冲区,工作!”,这时准备数据以进行下一次传输。考虑到显示器在流中接收数据的能力,出现了以下算法:突出显示前缓冲区,DMA将数据从前缓冲区传输到显示器,进行渲染的后缓冲区,以及用于深度切割的Z缓冲区。缓冲区是显示的单行(或列,无论如何)。而不是150 kB,我们只需要1920字节(每行320像素* 3个缓冲区*每点2字节),非常适合内存。第二次破解是基于以下事实:无法对每一行执行变换矩阵和顶点坐标的计算,否则图像将以最奇怪的方式失真,并且在速度上不利。相反,“外部”计算也就是说,在每个帧上重新计算变换矩阵的乘积及其在顶点上的应用,然后将其转换为中间表示,该中间表示针对渲染为320x1图片进行了优化。

由于流氓原因,该库从外部类似于OpenGL。就像在原始OpenGL中一样,渲染从形成转换矩阵开始-清除glLoadIdentity()使当前矩阵成为单位,然后清除一组转换glRotateXY(...),glTranslate(...),每个转换都与当前矩阵相乘。由于这些计算每帧仅执行一次,因此没有特殊的速度要求;可以省去简单的浮点数,而无需对定点数进行变换。矩阵本身是一个float [4] [4]数组,映射到一维float [16]一维数组-实际上,此方法通常用于动态数组,但您也可以从静态数组中获得一些好处。另一个标准技巧:与其不断计算旋转矩阵中的许多正弦和余弦,预先数数并写在平板电脑上。为此,将整个圆分成256个部分,为每个部分计算正弦值,然后将其转储到sin_table []数组中。好吧,学校里的任何人都可以从正弦得到余弦。值得注意的是,旋转函数在减小到[0 ... 255]范围后,并非以弧度为单位,而是以一整圈的分数为单位。但是,已实现“诚实”功能,可在引擎盖下执行从角度到波瓣的转换。在引擎盖下执行从角度到波瓣的转换。在引擎盖下执行从角度到波瓣的转换。

当矩阵准备就绪时,您可以开始绘制图元。通常,在三维图形中,存在三种类型的图元-点,线和三角形。但是,如果我们对多边形模型感兴趣,则应仅注意三角形。它的“渲染”发生在函数glDrawTriangle()或glDrawTriangleV()中。单词“ rendering”用引号引起来,因为在此阶段没有渲染。我们只将原始图元的所有点乘以变换矩阵,然后从中提取边y = ky * x +的解析公式,从而可以找到三角形的所有三个边与当前输出线的交点。我们丢弃其中一个,因为它不位于顶点之间的间隔上,而是位于顶点的延续上。也就是说,要绘制框架,您只需要遍历所有线条,并为每一个绘制相交点之间的区域即可。但是,如果您“正面”应用此算法,则每个图元将与之前绘制的图元重叠。我们需要考虑Z坐标(深度),以使三角形完美地相交。与简单地逐点打印不同,我们将考虑其Z坐标,并与存储在深度缓冲区中的Z坐标进行比较,或者输出(更新Z缓冲区),或者忽略它。为了计算我们感兴趣的线的每个点的Z坐标,我们使用相同的直线公式z = kz * y + bz,该公式由相同的两个带边的交点计算。结果,“半成品”三角形结构glTriangle的对象由三个顶点的X坐标组成(没有存储Y和Z坐标的意义,它们将被计算)和k,b直接系数,好,堆的颜色。在这里,与转换矩阵的计算相反,速度至关重要,因此我们已经使用了定点数。此外,如果对于项b,与坐标(2个字节)相同的精度已足够,则因数k的精度越高越好,因此取4个字节。但不是浮点数,因为即使使用相同的大小,使用整数仍会更快。

因此,通过调用一堆glDrawTriangle(),我们准备了一个半成品三角形数组。在我的实现中,通过显式函数调用一次推导出一个三角形。实际上,用顶点地址组成的三角形数组是合乎逻辑的,但是在这里我决定不复杂化。无论如何,渲染功能是由机器人编写的,对于他们来说,填写一个常数数组还是编写三百个相同的调用都无关紧要。现在是时候将三角形的半成品转换为屏幕上的精美图片了。为此,将调用glSwapBuffers()函数。如上所述,它经过显示的各行,搜索与所有三角形的每个交点,并根据深度过滤绘制线段。渲染每行之后,您需要将此行发送到显示器。为此,将启动DMA,它指示字符串的地址及其大小。同时,DMA有效,您可以切换到另一个缓冲区并渲染下一行。最重要的是,如果您突然提前完成渲染,不要忘记等待传输结束。为了可视化速度比,我在渲染结束后添加了一个红色LED,在DMA等待完成后添加了一个红色LED。事实证明,类似PWM的东西会根据延迟来调整亮度。从理论上讲,可以使用DMA中断代替“哑”等待,但是我不能使用它们,并且算法将变得更加复杂。对于演示程序,这是多余的。为了可视化速度比,我在渲染结束后添加了一个红色LED,在DMA等待完成后添加了一个红色LED。事实证明,类似PWM的东西会根据延迟来调整亮度。从理论上讲,可以使用DMA中断代替“哑”等待,但是我不能使用它们,并且算法将变得更加复杂。对于演示程序,这是多余的。为了可视化速度比,我在渲染结束后添加了一个红色LED,在DMA等待完成后添加了一个红色LED。事实证明,类似PWM的东西会根据延迟来调整亮度。从理论上讲,可以使用DMA中断,而不是“哑巴”的等待,但是我不能使用它们,算法将变得更加复杂。对于演示程序,这是多余的。

上述过程的结果是旋转了三个不同颜色的相交平面的旋转图片,并且速度相当不错:红色LED的亮度很高,这表明内核性能有很大的提高。

好吧,如果内核处于空闲状态,则需要加载它。我们将用更好的模型加载它。但是,请不要忘记内存仍然非常有限,因此控制器不会实际拉出太多的多边形。最简单的计算表明,减去行缓冲区之类的内存后,就有378个三角形的位置。如实践所示,古老但有趣的哥特式游戏中的模型非常适合此大小。实际上,从那里抽出了蛇和血蝇的模型(在撰写本文和glocoor时,已经在KDPV上炫耀了),然后控制器用尽了闪存。但是游戏模型不适合微控制器使用。

假设它们包含动画,纹理等,这对我们没有用,也不适合存储在内存中。幸运的是,blender不仅允许将它们保存为* .obj(更易于解析),而且还可以根据需要减少多边形的数量。此外,借助简单的自写程序obj2arr * .obj,将文件分类为坐标,随后从中形成* .h文件以直接包含在固件中。

但就目前而言,这些模型看起来就像普通的卷发污点。在测试模型上,这并没有打扰我们,因为所有的面孔都以自己的颜色绘制,但是没有为模型的每个多边形指定相同的颜色。不,您当然可以用随机的颜色绘制苍蝇,但我检查后看起来会很蓝。尤其是当每一帧的颜色也都发生变化时。...相反,应用另一滴矢量魔术并添加照明。

原始形式的照明计算包括计算法线和光源方向的标量积,然后乘以面部的“本机”颜色。
现在,我们拥有三种模型-从游戏中的两种模型和一种测试中开始。要切换它们,我们将使用焊接在板上的两个按钮之一。同时,您可以添加对处理器的控制。我们已经有了一个控件-与DMA延迟相关联的红色LED。第二个绿色LED会在每次更新帧时闪烁-因此我们可以估算帧率。肉眼大约为15 fps。


总的来说,我对结果感到满意:实施一些根本无法解决的事情很不错。当然,还有很多要优化和改进的地方,但是这没有什么意义。客观地讲,三维图形的控制器很弱,甚至与速度无关,而与RAM有关。但是,像任何演示场景样本一样,此项目的价值不在于结果,而在于过程。

如果突然对某人感兴趣,可以在此处获得源代码

All Articles