MBED,或有关泄漏的抽象

它朝mbed看了一眼。乍一看,它看起来非常有趣-一个C ++的独立于铁的框架,支持一堆微控制器和演示板,一个集成到版本控制系统中的在线编译器。一堆例子进一步证明了框架的优雅。使用已经实现的相应类,可以直接使用开箱即用的方式访问微控制器的几乎所有接口。直接从包装盒中直接使用C ++编写程序,而无需查看微控制器的数据表-这不是梦吗?

测试平台是受此平台支持的长期STM Nucleo F030。关于如何注册和启动第一个项目,有很多很好的教程,我们不再赘述。让我们直接去看有趣的东西。

该板上“板上”没有太多的外围设备。LED和按钮-就是所有的财富。好吧,第一个项目-来自微控制器领域的经典“ Hello world”-用LED闪烁。这是代码:

#include "mbed.h"
DigitalOut myled(LED1);
int main() {
    while(1)
    {
        wait_ms(500);
        myled = myled ^ 1;
    }
}

毕竟还是不错,不是吗?您甚至根本不需要研究控制器数据表!

顺便说一下,经典的“ Hello world”也可以立即使用:

#include "mbed.h"
Serial pc(USBTX, USBRX);
int main() {
    pc.printf("Hello world\n\r");
    while(1)
    {
    }
}

真的不是很好吗? “开箱即用”我们是否有一个可以在其中进行标准系统打印的控制台?顺便说一下,端口在板子连接到计算机时也随即出现,同时还有一个虚拟磁盘,您只需要在其上复制组装好的二进制文件-板子就会自行重启。

但是回到闪烁的LED。编译,下载到开发板上-闪烁!但是以某种方式太快了,显然每秒超过一次,但至少是5次...糟糕...

标题中提到的“有孔抽象”让我有些分心。我曾与Joel Spolsky一起读过这个词。这是他的名言:如果我教C ++程序员,那我不需要告诉他们有关char *和指针算法的话,那就太好了,但是我可以直接参考标准模板库中的内容。但是有一天他们会写“ foo” +“ bar”,并且会出现奇怪的问题,但是我仍然必须向他们解释char *是什么。否则他们将尝试使用LPTSTR类型的参数调用Windows函数,并失败,直到他们学习char *和指针以及Unicode和wchar_t和TCHAR头文件-所有这些都通过抽象的洞洞而显现出来。

因此,有悖常理的是,有漏洞的抽象定律不允许人们接受并开始对这样的微控制器进行编程(即使这是三行中最简单的“ hello world”),而无需查看其数据表并且不知道微控制器具有相同的内部,以及所有这些抽象类背后的功能。与营销商的承诺相反。

因此,在叹了口气之后,我们开始寻找。如果在我看来,它不是控制器的完整抽象环境,而是一个完整的开发环境,那么在哪里进行挖掘就很清楚了。但是在上面的代码的情况下该从哪里挖掘呢?如果甚至mbed.h文件的内容也不允许显示抽象?

幸运的是,mbed库本身是开源的,您可以下载它并查看其中的内容。再次,幸运的是,通过对程序员的抽象,尽管未明确声明微控制器的所有寄存器,但它们都是可访问的。已经更好了。例如,您可以通过运行以下命令来验证我们是否具有时钟速度(为此,我必须查看数据表并对其进行深入研究):

  RCC_OscInitTypeDef str;
  RCC_ClkInitTypeDef clk;
    pc.printf("SystemCoreClock = %d Hz\n\r", HAL_RCC_GetSysClockFreq());
    
    HAL_RCC_GetOscConfig(&str);
    pc.printf("->HSIState %d\n\r", str.HSIState);
    pc.printf("->PLL.PLLState %d\n\r", str.PLL.PLLState);
    pc.printf("->PLL.PLLSource %d\n\r", str.PLL.PLLSource);
    pc.printf("->PLL.PLLMUL %d\n\r", str.PLL.PLLMUL);
    pc.printf("->PLL.PREDIV %d\n\r", str.PLL.PREDIV);
    pc.printf("\n\r");
    
    HAL_RCC_GetClockConfig(&clk, &flat);
    pc.printf("ClockType %d\n\r", clk.ClockType);
    pc.printf("SYSCLKSource %d\n\r", clk.SYSCLKSource );
    pc.printf("AHBCLKDivider  %d\n\r", clk.AHBCLKDivider );
    pc.printf("APB1CLKDivider %d\n\r", clk.APB1CLKDivider );


一切都与频率相吻合;按照预期,频率为48 MHz。
好吧,进一步挖掘。我们尝试连接一个计时器而不是wait_ms():

   Timer timer;
    timer.start();
    while(1) {
        myled ^= 1;
        t1 = timer.read_ms();
        t2 = timer.read_ms();
        while (t2 - t1 < 500)
        {
            t2 = timer.read_ms();
        }
    }


很漂亮,不是吗?但是LED仍然比期望的闪烁速度快5倍...

好的,我们更深入地研究一下魔术计时器背后的功能,正如文档所承诺的那样,无论有多少个硬件计时器,都可以创建任意数量的魔术计时器微控制器。不错,是的...

对于STM F030,TIM1控制器硬件计时器隐藏在其后方,可对微秒级滴答进行编程。在此基础上,其他所有内容均已构建。

所以,已经很热了。还记得我说过所有寄存器都可用吗?我们来看以下内容:

pc.printf("PSC: %d\n\r", TIM1->PSC);


瞧!这样就结束了对谁的责任的调查:将数字7发送到控制台,在PSC寄存器中(写吗?这真的只是一个巧合吗?)有一个值,到达该值时计时器将生成中断并重新开始计数。并在此中断和微秒计数器挂起。这样一来,在48 MHz的频率下,中断每毫秒发生一次,那么根本就不应有7个中断,而应该是47个。在加载微控制器的第一阶段,很可能有7个中断到达那里,因为它以8 MHz开始,然后重新调谐PLL,以便将频率乘以6,得到所需的48 MHz。计时器似乎初始化得太早了……
当然,应该责怪谁。但是该怎么办?对该框架的挖掘导致了以下调用链:

首先:SetSysClock_PLL_HSI()-> HAL_RCC_OscConfig(),HAL_RCC_ClockConfig()-> HAL_InitTick()-更改频率时,调用设置微秒刻度的函数。

第二个:HAL_Init()-> HAL_InitTick()-> HAL_TIM_Base_Init()-> TIM_Base_SetConfig()-> TIMx-> PSC-从最全局函数之一调用,HAL_InitTick根据当前时钟将所需的值写入PSC寄存器频率...
对我来说,一个谜仍然是,对于STM F0系列,第二条链没有被调用。我没有找到对HAL_Init()函数的任何调用!

此外,如果您看一下HAL_InitTick()的实现,那么在其开始时将有以下几行:

    static uint32_t ticker_inited=0;  
    if(ticker_inited)return HAL_OK;
    ticker_inited=1;


只需调用一次此函数。也就是说,您可以根据需要多次呼叫她,但是她以后的所有呼叫都不会做。而且在时钟频率发生变化时框架调用它的事实是没有用的……

我懒得重建库,所以改成这种形式:主函数的第一行是这样的:

TIM1->PSC = (SystemCoreClock / 1000000) - 1;


之后,LED终于开始以正确的1 Hz频率闪烁...

我不知道假设的某个人将停止在哪个阶段,谁让mbed营销人员说服了他们,试图从头开始使微控制器编程如此容易?如果他以前没有遇到过微控制器?

好吧,如果我不是很累,那就继续吧。使LED闪烁是好的,但并不有趣。我想要更多。 DS1820温度传感器是从一个较大的传感器中找到的,并为其制成了一个完整的库。易于使用,隐藏了内部使用该传感器的复杂性。所有需要做的就是写类似

#include "DS1820.h"
DS1820 probe[1] = {D4}; // D4 –  ,    
probe[0].convert_temperature(DS1820::all_devices);
float temperature = probe[0].temperature('c');


您认为在编译和启动后发生了什么?正确地。它没有用:)

抽象泄漏,是的。好吧,似乎还需要对mbed内部进行另一项令人兴奋的研究。以及传感器本身的详细信息。

传感器非常有趣。通过其数字接口。在一行中在半双工模式下工作。控制器启动数据交换,传感器作为响应发送一系列位。尤其是对于此类情况,mbed框架具有DigitalInOut类,您可以使用该类即时更改其在GPIO引脚上的工作方向。像这样:

bool DS1820::onewire_bit_in(DigitalInOut *pin) {
    bool answer;
    pin->output();
    pin->write(0);
    wait_us(3);                 
    pin->input();
    wait_us(10); 
    answer = pin->read();
    wait_us(45); 
    return answer;
}


控制器向传感器发送脉冲“ 1-> 0-> 1”,这是传感器响应发送一个位的信号。什么可能在这里不起作用?这是传感器数据表的一部分:



如您所见,在向传感器发送脉冲后,为了读取该位,我们有多达15微秒的时间。

我们连接了示波器,并看到传感器向自身发送位,如数据表中所示。但是在这里,控制器读取垃圾。可能是什么原因?

关于如何执行此代码,我不会写太多:

    pin->output();
    t1 = timer.read_us();
    pin->input();
    t4 = timer.read_us();
    pc.printf("Time: %d us\n\r", t4-t1);


好吧,谁猜不出进一步的阅读,如果使用写入的mbed框架完成操作,则在一个时钟频率为48 MHz的微控制器上改变一个GPIO引脚的方向需要花费多少时间(实际上,如果写入,则一次写入寄存器)。 C ++使用铁独立层?

13微秒。

要读取传感器的响应,我们有多达15个。即使从上面的代码中完全删除了wait_us(10),命令answer = pin-> read();还需要一些超过2微秒的时间。没有时间阅读答案就足够了。

幸运的是,可以在不改变GPIO方向的情况下向STM发送脉冲。在输入模式下,连接内置上拉电阻时,效果是相同的。再一次,幸运的是,调用pin->模式(PullUp)而不是pin-> input()仅需要6微秒。

有足够的时间来阅读答案。

最后,抽象中的另一个漏洞。 DS1820工作之后,即将推出的下一个传感器是DHT21,该传感器可同时测量温度和湿度。同样具有1线接口,但是这次是带有脉冲持续时间的编码响应。似乎显而易见的解决方案是将这条线挂在GPIO输入中断上,这使测量脉冲持续时间变得容易。甚至在mbed中都有一门课程。而且它甚至可以按照记录进行工作。

但是问题在于该传感器还可以在半双工模式下工作。为了使他开始传输数据,他需要发送一个脉冲“ 1-> 0-> 1”,如上例所示。

而且这里mbed不允许这样做。或者您将端口声明为DigitalInOut,但是您不能使用中断。或者,您将端口声明为InterruptIn,但是您无法向其发送任何内容。不幸的是,PullDown技巧在这里不起作用。传感器具有内置的PullUp,内置的PullDown微控制器不足以将引脚拉至0。结果,

我不得不减少漂亮的线路轮询周期。当然,所有内容都可以那样工作,但效果并不理想。直接访问寄存器不会有问题...

这里是。尝试进行抽象,以便任何人都可以立即开始编写值得的微控制器。许多事情确实大大简化了,甚至可以工作。但是,像任何高级抽象一样,这种抽象也充满了漏洞。在与众多洞中的任何一个发生碰撞时,它们必须从天堂降落到人间。

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


All Articles