技术宅的结界

 找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 292|回复: 7
收起左侧

【单片机】神经病操作——使用STM32F103驱动VGA

[复制链接]

1060

主题

2443

帖子

6万

积分

用户组: 管理员

一只技术宅

UID
1
精华
221
威望
348 点
宅币
19512 个
贡献
40290 次
宅之契约
0 份
在线时间
1844 小时
注册时间
2014-1-26
发表于 2020-6-11 05:44:33 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有帐号?立即注册→加入我们

x
与VGA信号相关的内容请看上一篇帖子:【FPGA】我的FPGA学习小结:用FPGA控制SDRAM,并通过VGA控制显示屏

STM32F103是STM32系列单片机里比较低配芯片了,工作频率是72 MHz,虽然可以超频到128 MHz,然而意义不大。

本来,单片机这种东西,是与VGA“绝缘”的。单片机不管是性能还是它自己带的功能都不足以满足VGA输出的根本需求,并且在实际的应用场景下,单片机基本与VGA无关——VGA需要接口双方都具备极高的实时性,而单片机则可以在不需要实时性的情况下控制一个串口屏进行画面的绘图和显示。

要问我为何突发奇想要用单片机实现串口屏的驱动?因为有人告诉我“单片机驱动VGA是不可能的”。但,好像并不是这样?至少似乎没有前人尝试过,所以我打算试试。

根据STM32F103单片机的运行能力,72 MHz,似乎可以支持 640x480 分辨率、60 Hz刷新率的显示模式的。在这个显示模式下,根据资料,每一帧在包含了空白区域后的总像素数量是800 x 524。这个数字是这么来的:

可见像素数 + 前porch + 同步区域 + 后porch = 总像素数
640 + 16 + 96 + 48 = 800
480 + 11 + 2 + 31 = 524

其中,“同步区域”指的是HSYNC或者VSYNC信号线在低电平的时间。如下图所示。

vgaporch.png

我们输出像素的时候,是从左到右从上到下一个个按顺序输出像素的。显示器就是通过判断这两个SYNC信号的长度和位置来判断你的输出分辨率和扫描行数。并且我们需要在经过非可见区域的时候,输出黑色的像素,用来帮显示器校准颜色值的范围。毕竟VGA传输的是模拟信号,而不是数字信号。

所以只要我们能用单片机精确驱动HSYNC和VSYNC信号线,就能让显示器亮起来。在这之后,我们考虑使用DMA把想要显示的数据按字节以恒定速度输出到A0-A7口上,并使用电阻矩阵来实现DA转换后得到模拟信号,然后通入VGA的RGB三色信号线来实现像素颜色输出。

我看了一下VGA的接口和线,我感觉我并不想破坏线,或者焊接线到接口上。这玩意儿需要有个插座,然后我把线焊接到插座上就好。

IMG_4753.png

于是我上淘宝随便买了一些像这样的插座。

IMG_4754.png

经过一番折腾,我做了一个洞洞板,它上面有我要的电阻矩阵。并且我按照计划把A0-A7作为像素颜色输出(高3bit是蓝色,中3bit是绿色,低2bit是红色),把计时器1通道1的A8线接到VSYNC上,把计时器2通道1的A15线接到HSYNC上,然后把GND全部并联到一起(本来VGA传输协议里不同的接地要分开,并且红绿蓝三组信号线每一组都和自己的接地是一个差分传输对,用于降低信号干扰并提高能用的频率),硬件部分差不多就算做完了。

IMG_4755.jpg

接下来是软件上的安排。我其实最初的计划是打算使用这玩意儿通过VGA让屏幕输出80x25字符界面,并通过串口输入来更新屏幕内容。但实际上这是美梦一场。

CubeMX.png

软件上的实现,我打算直接使用TIM计时器的PWM方式以精确到时钟的精度控制HSYNC和VSYNC数据线来亮屏。然后,我计划使用一个内存到内存DMA来保证匀速地输出像素从而实现正常地显示。

我把计时器TIM1设置为Master模式,是为了让它能带着TIM2同时计时。而TIM1负责VSYNC信号和帧内定时中断,TIM2负责HSYNC信号和行内的定时中断。

那么这两个定时器应该用什么样的频率或者周期来工作比较合适呢?

首先回到之前我们说的总像素数,800 x 524,其中 800 是一行扫描线的像素数量,524 是一屏画面的扫描线数量。总的像素数量是 800 x 524 = 419200 像素,假设刷新率为每秒 60 帧,那么每秒需要输出的总像素数量是:

800 x 524 x 60 = 25152000

我们的STM32F103时钟默认是72 MHz,也就是72000000 Hz,约等于总像素数量的三倍。相当于平均下来我们每3个周期就要输出新的像素

我们来算一下误差。

72 MHz / 3 = 24 MHz

如果真的以24 MHz的频率输出像素,我们得到的实际帧数是:

24 MHz / 419200 px = 57.251908396946564885496183206107 Hz

57Hz.png

还可以,十分接近60 Hz的刷新率,显示器应该会支持

按照我的推测,DMA如果每个周期都能进行一次内存访问和操作,它完全可以达到这样的速度,并且软件核心上还可以有足够的余裕周期用来做其它的事情。

虽然实际上,以每像素8bit位深来存储图像的话,STM32F103的RAM大小完全不够存储一帧画面的。但是我们可以临场绘图呀,画一行显示一行就行了。画的过程交给处理器,然后输出扫描行的过程交给DMA。

这也就是为什么一开始我觉得完全可以实现一个 80x25 彩色文本模式屏显功能的原因之一。

回到主题。我们需要能在精确到扫描行的情况下,控制PWM输出VSYNC信号,或者触发中断用于通知程序知道当前输出的垂直位置。我们有总共524个扫描行,我们的计时器需要的频率的计算方式如下:

57.251908396946564885496183206107 Hz x 524 lines = 30000 Hz

30000.png

那么我们的时钟的Prescaler应该是72 MHz / 30000 Hz = 2400 周期。

所以TIM1的Prescaler是2400(其实写入寄存器的是2399,要减去1。理由文档有说明)

我们的定时器1有三个通道,第一个是PWM,用于输出VSYNC信号(模式是“PWM Generation CH1”)。第二个是用来产生中断、提示进入Y有效范围(模式为“Output Compare No Output”)。第三个也是用来产生中断,提示进入Y无效范围(模式和第二个一样)。我原先计划在Y无效范围内(闲下来的时候)处理诸如USART、USB等外设的请求。第四个通道用不到。

因为我们需要在启用TIM1的同时启用TIM2,后者被用于输出扫描行和HSYNC信号。所以我们设置Trigger Output功能,把TIM1设为Master,用于触发Slave,在CNT_EN事件下触发。

当前分辨率下我们的垂直同步信号的长度只有2扫描线的长度,那么我们的PWM通道的信号长度应该设置为如下:

524 - 2 = 522

并且我们在经过11个扫描行后进入垂直有效区域,所以通道2的Pulse是11。通道3是我们进入垂直无效区域的时间。

TIM2是用来输出扫描行和HSYNC信号的。同样有3个通道,且第一个通道用来输出PWM实现精确的HSYNC信号计时。为了能被TIM1触发,我将其设置为Slave模式Trigger Mode,并且触发源是ITR0(对应TIM1)。因为我们每3个周期输出一个像素,所以我们的TIM2的周期是:

800 x 3 = 2400

HSYNC区域的像素数是 96 px,也就是 288 周期,那么PWM通道的信号长度是:

2400 - 288 = 2112 周期

然后我们的第二个和第三个通道用于触发中断。我们在中断ISR里启动DMA进行图像的绘制,并在到达扫描线结尾的无效区域的时候停止DMA传输。

我原先计划过直接用TIM2的第2、3通道触发DMA进行拷贝。这个功能本身是被设计用于驱动步进电机的,所以它触发DMA后,DMA只会进行一个单元(字节、HalfWord、Word)的复制。我想利用这一次DMA的触发,看看能不能间接触发另一个内存到内存的DMA,使其开始一整行的像素输出,像这样:

1、我设置内存到内存DMA的源、目标、各项属性、计数器CNDTR
2、我用TIM2在计数器输出比较的时候触发计时器硬件DMA,而这个DMA会把一个我预先设置好的值从内存某处复制到我的内存到内存通道的CCR寄存器地址上。
3、内存到内存DMA被激活,它开始输出扫描行。当它结束了以后,它触发中断。我在中断处理程序里,设置它的计数器寄存器CNDTR,以便于下一次被激活

理论上可行,而且我在别的项目里试过这样的连锁操作,并没有问题。但是搬到这个非常吃时钟的项目里,它触发不了DMA,也触发不了中断

所以我改变了主意,干脆在TIM2计时器的中断里直接进行软件上的启动DMA过程

但是在进行实际的像素显示之前,我打算先试试看能不能亮起屏幕来,所以我直接把一个不断递增的整数输出到A0-A7口上(GPIOA->ODR),然后它像这样显示:

flashing.jpg

看静态图可能感受不到啥,虽说红绿蓝三种颜色都看得出来(这证明了我的电阻矩阵焊接得还可以),但这个锯齿的规律令人摸不着头脑,并且实际上绿色部分非常闪烁,简直让人瞎眼。




随后,我决定开始正经绘图了。我首先准备了一个行缓冲区,用于存储一行的像素。因为STM32F103CBT6的RAM只有20K,所以不太可能存储一整屏的帧缓冲。

假设DMA每个周期都可以输出一个像素,那么我的行缓冲区的大小应该是三倍于可视范围宽度(640 px)的,其中每3个像素颜色相同。这样做,可以让DMA保证以正确的速度输出像素。

但实际上,我在前文对于DMA的能力方面做出来的假设都不太对——实际上,我只要操作了内存,都会让DMA暂停一个周期的工作,不复制数据。这本来就是为何DMA有优先级设定的由来。

而且STM32CubeMX在使用SysTick进行计时的过程都会严重影响我从启动DMA开始的后续输出像素的时间精确度

所以当我打算一边绘制行缓冲(绘制方法是把红色、绿色、蓝色根据X位置、Y位置、帧计数等做成一个像素),一边用DMA把行数据拷贝到GPIOA->ODR的时候,我看到了“巨大的像素”……




为了排除问题在哪,我当时减少了行缓冲的长度,看看能不能通过减少要复制的数据的量来让它改变显示的方式,结果画面确实发生了变化。

vgatest.jpg

虽说左边的空白区域暂时先不管了,毕竟就算是触发了中断->软件开始准备启动DMA->写了一系列寄存器让DMA终于有理由工作后,DMA自身能不能立即读写内存还是个问题。

我其实尝试过调整中断发生的时间,看看把它提前或者延后会怎么样。然并卵。这个空白区域的面积根本就没变。

不过画面上看起来是非常流畅的就是了。




后面经过了我的多次的尝试和修改,包括尝试纯软件输出像素、后,我发现要想让它能够输出字符是不太可能的了。于是我计划让它输出静态图

输出静态图比输出动态图有两个好处,一个是静态图可以被存储到ROM里,而STM32F103CBT6的ROM是 128 KB 足以容纳一定大小的图。

但,我们需要把静态图转换为能够以R2G3B3方式输出的格式才行。此时我使用了Adobe Photoshop把图片大小缩小到64 x 48,减小了十倍。这是因为我发现使用DMA输出像素的过程能明显把像素拉长。

此外,我先把最右一纵列的所有像素设为全黑,以帮助我省略空白区域输出全黑像素的操作。

因为是8bit颜色深度,所以我可以使用一个序列和位编码完全一致的调色板来让Photoshop把图片适应到这样的颜色格式里。我随手写了个软件用来实现这个目标。

vb6pal.png

再把这个调色板导入到PS即可。导入方式也很简单。PS支持一个叫“ACT”的格式的调色板。你只需要把红绿蓝三个字节紧凑排列,并提供256种颜色,做成一个768字节的裸文件,改后缀为ACT即可。

pspal.png

然后设置合适的抖动算法,比如扩散抖动,可以降低图像失真。接下来保存为BMP。

good.png

注意一定记住:保存BMP的时候,要设置“反转行序”,不然你得到的图像数据在存储上就是上下颠倒的。

flip.png

之后我们就可以从BMP里面提取它的像素数据了。同样很简单直接用二进制修改工具打开它,然后找出其中存储调色板的部分的数据,跳过它,底下就是纯的像素位点数据了。(只要你熟悉BMP格式,这都不是问题

pixdata.png

再使用你的二进制修改工具复制出这段数据的C语言数组形式代码,就可以拿到你的代码里去用了。

NPP.png

bytes.png

得到图片以后,我们需要调整绘图的方式。此时我改成在主循环里根据扫描线的位置,提供原图的像素行数据的指针,并让时钟中断直接根据指针来启动DMA的传输,实现稳定的图像输出。需要注意的是:时钟中断里的“if平衡性”需要得到保证,有时候if分支占的周期多少不一会导致看到的像素左右偏移,产生锯齿。我最终通过去掉了多余的if解决了这个问题。

效果还是可以的:

IMG_4752.jpg




使用示波器观察我的电阻矩阵的输出效果。能看到有很多Spike。这或许就是为什么STM32F1频率不能太高的原因吧。




代码:

vga640x480.zip (502.84 KB, 下载次数: 0, 售价: 5 个宅币)
回复

使用道具 举报

1060

主题

2443

帖子

6万

积分

用户组: 管理员

一只技术宅

UID
1
精华
221
威望
348 点
宅币
19512 个
贡献
40290 次
宅之契约
0 份
在线时间
1844 小时
注册时间
2014-1-26
 楼主| 发表于 2020-6-11 06:21:43 | 显示全部楼层
经过测试,发现只要以Release方式编译,并应用gcc的-O3 -flto优化,就可以实现图片顶头显示,完全没有问题。

这说明Debug无优化浪费的时钟太多。

fixed.jpg

0

主题

1

帖子

24

积分

用户组: 初·技术宅

UID
5969
精华
0
威望
6 点
宅币
11 个
贡献
0 次
宅之契约
0 份
在线时间
1 小时
注册时间
2020-6-11
发表于 2020-6-11 10:04:10 | 显示全部楼层
专门注册账号来给大佬捧场

1060

主题

2443

帖子

6万

积分

用户组: 管理员

一只技术宅

UID
1
精华
221
威望
348 点
宅币
19512 个
贡献
40290 次
宅之契约
0 份
在线时间
1844 小时
注册时间
2014-1-26
 楼主| 发表于 2020-6-11 18:08:05 | 显示全部楼层
myuan 发表于 2020-6-11 10:04
专门注册账号来给大佬捧场

多谢捧场

28

主题

185

帖子

2073

积分

用户组: 版主

UID
1821
精华
6
威望
67 点
宅币
1610 个
贡献
114 次
宅之契约
0 份
在线时间
345 小时
注册时间
2016-7-12
发表于 2020-6-11 20:55:18 | 显示全部楼层
把数据处理成行,然后隔行扫描

28

主题

185

帖子

2073

积分

用户组: 版主

UID
1821
精华
6
威望
67 点
宅币
1610 个
贡献
114 次
宅之契约
0 份
在线时间
345 小时
注册时间
2016-7-12
发表于 2020-6-11 20:58:41 | 显示全部楼层
另外普通vga线 gnd都是焊道一起的 只有全彩线,或者工控线才分开,很粗,比高清hdmi还粗

0

主题

1

帖子

23

积分

用户组: 初·技术宅

UID
5973
精华
0
威望
6 点
宅币
10 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2020-6-12
发表于 2020-6-13 00:59:51 | 显示全部楼层
看完了楼主的大作,我这种没刷过VGA信号的菜鸡感觉有点厉害。F103刷VGA还是比较累的,我想了想,似乎还能抢救一下:
1.给F103超频,超频不超过50% 应该是OK的。
2.看到图片似乎是写到Flash里了,我记得Flash速度不是很快,可能会浪费掉一些时间?
3.F103是否能实现DMA双缓冲?
4.似乎没必要所有时间都在刷VGA,可不可以“跳帧”输出? 只要输出够人眼难以察觉到的帧数就可以了,让单片机能有短暂的空闲时间处理处理其他数据,比如游戏输入操作。

对VGA并没啥了解,本来想说俩DMA一起输出,结果查了下F103好像就一个DMA。像H750这种都有很多DMA通道,甚至还有D-Cache啥的,能不停的吐,刷VGA估计简单的不行。
HAL库的操作还是很繁琐的,或许直接草寄存器能刷的更快一些。cubemx真香。

1060

主题

2443

帖子

6万

积分

用户组: 管理员

一只技术宅

UID
1
精华
221
威望
348 点
宅币
19512 个
贡献
40290 次
宅之契约
0 份
在线时间
1844 小时
注册时间
2014-1-26
 楼主| 发表于 2020-6-13 23:02:06 | 显示全部楼层
mo10 发表于 2020-6-13 00:59
看完了楼主的大作,我这种没刷过VGA信号的菜鸡感觉有点厉害。F103刷VGA还是比较累的,我想了想,似乎还能抢 ...

1、超频应该是可以的。
2、好像速度和RAM一样快
3、能是能,但同时开启的两个内存到内存DMA是争夺访问权的,它们只能轮流访问内存。相当于只开一个内存到内存DMA。
4、不可以,直接黑屏。而且事实上我这个帖子就是只刷静态图片那一块,别的地方就让A0-A7口留0值了。

直接草寄存器或者写ASM确实可以实现较为复杂一点的功能,因为更加底层,可以做到卡着时钟周期来执行特定的任务,刷屏甚至都不需要DMA了。我知道有人使用STC实现VGA输出俄罗斯方块,他的做法就是手写汇编。

CubeMX真香。

本版积分规则

QQ|申请友链||Archiver|手机版|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图  

GMT+8, 2020-7-13 14:01 , Processed in 0.126575 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表