有时,其他人的代码确实有助于将外围设备连接到微控制器。不幸的是,要使他人的代码适应您的项目可能比自己重写它更困难,尤其是在涉及到诸如arduino或mbed之类的大型框架时。想要将基于ILI9341的中文LCD连接到STM32L476G DISCOVERY板上,作者着手使用ST演示项目中为mbed编写的驱动程序,而无需更改其代码中的任何一行。结果,可以同时将屏幕超频到前所未有的27 fps更新速度。
问题简介
意法半导体(ST Microelectronics)生产的微控制器在功能和价格上都很有趣,并且还发布了可快速开发的电路板。将讨论其中之一-STM32L476G DISCOVERY。该板的计算能力非常令人鼓舞-最大时钟频率为80MHz的32位ARM可以执行浮点运算。同时,它能够将功耗降至最低并依靠电池工作,等待机会做一些有用的事情。我决定将一个通过SPI接口工作的,分辨率为320 x 240的廉价中文彩色LCD连接到该设备。此处将详细介绍如何与mbed一起使用它。床-这是一个在线编程环境,您可以在不使用编译器的情况下编译固件,然后只需将其复制到与mbed兼容的板中即可下载并刷新固件,该板与USB连接时看起来像可移动磁盘。所有这些都很棒,但是有一些问题。首先,并非所有主板都兼容mbed。其次,存在许多与mbed根本不兼容的现有项目,包括ST提供的软件。最后,并不是所有的开发人员都与mbed兼容,某些人(例如,这些行的作者)在这个奇妙的工具中发现弊大于利。这些缺点是什么,我们将在下面进行讨论,现在足以说明在连接显示驱动程序之后 从ST到演示项目以及一些简单的优化,它的运行速度快了大约10倍。学习驱动程序代码
现在该下载并研究显示驱动程序的源代码了。通过调用代表I / O端口的类方法来组织使用mbed中的端口。例如,DigitalOut类实现对输出端口的访问。将此对象的实例分配为零或一个,将启动将相应位写入输出端口的操作。 DigitalOut类由枚举类型PinName初始化,其唯一目的是识别处理器分支。实现DigitalOut和其他实现I / O的类的主要缺点之一是在类实例的构造函数中初始化了端口。如果在主函数中的堆栈上创建了DigitalOut类的实例,这对于使LED闪烁非常有用。但是想象一下,我们有很多不同的铁,它们的初始化分散在几个模块中。如果我们将I / O类的实例设为静态,则我们将失去对初始化的所有控制权,因为它会以任意顺序出现在main函数之前。 ST库(称为HAL-硬件抽象级别)使用其他更灵活的范例。每个输入/输出端口都有其自己的上下文以及与之配合使用的一组功能,但是可以在需要时准确地调用它们。端口上下文通常创建为静态变量,但是不会发生自动不受控制的初始化(ST库用C编写)。一个非常方便的实用程序也值得一提。ST库(称为HAL-硬件抽象级别)使用其他更灵活的范例。每个输入/输出端口都有其自己的上下文以及与之配合使用的一组功能,但是可以在需要时准确地调用它们。端口上下文通常创建为静态变量,但是不会发生自动不受控制的初始化(ST库用C编写)。一个非常方便的实用程序也值得一提。ST库(称为HAL-硬件抽象级别)使用其他更灵活的范例。每个输入/输出端口都有其自己的上下文以及与之配合使用的一组功能,但是可以在需要时准确地调用它们。端口上下文通常创建为静态变量,但是不会发生自动不受控制的初始化(ST库用C编写)。一个非常方便的实用程序也值得一提。但在这种情况下,不会发生自动的不受控制的初始化(ST库用C编写)。一个非常方便的实用程序也值得一提。但在这种情况下,不会发生自动的不受控制的初始化(ST库用C编写)。一个非常方便的实用程序也值得一提。CubeMX可以为您需要的端口集生成所有必要的初始化代码,甚至允许您随后在不影响自己的代码的情况下对这组端口进行更改。唯一的缺点是无法与现有项目一起使用,必须使用此实用程序启动项目。mbed库使用与ST库相同的HAL函数来初始化微控制器的资源,但这使它在某些地方显得毫无思想可言。为确保这一点,只需在spi_api.c文件中查看SPI端口的初始化代码(我们需要使用显示器)即可。。spi_init函数首先通过将要使用的分支来寻找合适的SPI端口,然后调用init_spi函数,该函数实际上是初始化该端口的。同时,所有三个可能的SPI端口都使用一个静态上下文结构static SPI_HandleTypeDef SpiHandle;
本质上,这是使用全局变量而不是局部变量的经典示例。即使考虑到我们只有一个计算核心这一事实,也无法防止全局上下文在代码的不同位置同时使用,但仍然存在中断,以及挤出多任务。将库连接到您的项目
所以我不想在mbed上编写所有代码。我更喜欢CubeMX附带的ST示例。我没有找到用于ST库的LCD的最终驱动程序,我不想自己编写它。仍然有一种有趣的替代方法-连接为mbed编写的驱动程序,从而无需进行任何更改。您需要做的就是以替代方式实现mbed库。实际上,该任务比看起来要简单,因为所有的mbed库,LCD驱动器仅使用输出端口和SPI。另外,他需要延迟的形成功能以及文件和流的类。对于后者,一切都很简单-我们不需要它们,而由无能为力的插头代替。延迟生成函数易于编写-它们在文件中wait_api.h。实施I / O类需要稍微更具创造性的方法。我们将解决缺少mbed库的问题,而不是在构造函数中进行硬件初始化。构造函数将接收到位于其他位置的端口上下文的链接,其初始化代码将完全独立于我们的接口类。唯一的方法是在不更改驱动程序代码的情况下将信息传递给构造函数-通过PinName,它现在不再简单列出分支,而是存储指向端口的指针,分支的编号以及可选的指向该分支所连接的资源(如SPI)的指针。class PinName {
public:
PinName() : m_port(0), m_pin(0), m_obj(0) {}
PinName(GPIO_TypeDef* port, unsigned pin, void* obj = 0)
: m_port(port), m_pin(pin), m_obj(obj)
{
assert_param(m_port != 0);
}
GPIO_TypeDef* m_port;
unsigned m_pin;
void* m_obj;
static PinName not_connected;
};
输出端口的实现非常简单。为了提高性能,我们将尽量减少使用HAL函数,并尽可能地直接使用端口寄存器,并编写内联代码,这将使编译器避免调用函数。class DigitalOut {
public:
DigitalOut(GPIO_TypeDef* port, unsigned pin)
: m_port(port), m_pin(pin)
{
assert_param(m_port != 0);
}
DigitalOut(PinName const& N)
: m_port(N.m_port), m_pin(N.m_pin)
{
assert_param(m_port != 0);
}
void operator =(int bit) {
if (bit) m_port->BSRR = m_pin;
else m_port->BRR = m_pin;
}
private:
GPIO_TypeDef* m_port;
unsigned m_pin;
};
SPI端口实现代码并不复杂,您可以在此处查看。由于我们将端口的初始化与接口代码分开,因此我们忽略了对配置更改的请求。只需记住单词的位深度即可。如果用户要传输16位字,并且端口配置为8位,则我们只需要重新排列字节并一一传输它们-最多4个字节仍放置在端口缓冲区中。编译驱动程序所需的所有文件都收集在compat目录中。现在,您可以将原始驱动程序文件连接到项目并进行编译。我们还需要一个代码来初始化端口,创建驱动程序实例并在屏幕上绘制一些有意义的东西。超频
如果使用LCD输出动态内容,那么自然就有希望与之进行更快的通信。首先想到的是增加SPI时钟频率,驱动器将其设置为10MHz,但是我们无视他的意愿,可以进行任何设置。事实证明,该屏幕可以在40MHz的频率下正常工作-这是我们的时钟频率为80MHz的处理器能够达到的最大频率。为了评估性能,编写了一个简单的代码,该代码在循环中显示100x100像素的位图。然后将结果外推到整个屏幕(占据整个屏幕的位图根本不适合内存)。结果-11fps与32fps的理论极限相差甚远,如果您不停止地将16位传输到每个像素,则可获得32fps的理论极限。如果你看一下,原因就很清楚了驱动程序源代码。如果他需要传输一个字节序列,则只需简单地一个接一个地传输它们,最好将其打包成16位字。这种低效设计的原因在于mbed API。SPI类具有一种用于传输数据数组的方法,但只能异步使用,在完成时以及在中断处理程序的上下文中调用通知函数。很少有人使用这种方法也就不足为奇了。我用发送缓冲区并等待传输结束的函数补充了SPI类的实现。在对位图传输代码添加对该函数的调用后,性能提高到27fps,这已经非常接近理论极限。源代码
躺在这里。为了进行编译,使用了用于ARM 7.50.2的IAR嵌入式工作台。基于ST的代码演示固件。LCD连接到的引脚的描述可以在lcd.h文件中找到。