Ctrl-FOC-Lite复刻记录

失败!
最后还是没有成功,以焊的两片板子都出现电源反灌烧毁而告终,还带走了我的PW-LINK。
后来者请谨慎入坑

基于磁场定向控制(FOC)的电机驱动技术已成为机电系统高精度运动控制领域的核心研究方向之一。本文立足于对开源项目 Ctrl-FOC-Lite 的复刻实践,对无刷电机控制技术进行直观的学习、理解和应用。

想当初,我以为只是让电机转起来很简单,用基于 Arduino 框架的 SimpleFOC 就更简单了。但事实表明,我在这里花了远多于预期的时间,太!菜!了! 因此写了本文记录过程和相关启发与思考。

(遇到的问题中,相当多的部分是由于环境的抽象程度太高 + stm32 不是原生平台,难免遇到一些兼容性问题)

但是有一说一, C++ 写起来确实比C爽

注意
原作者在面积压缩上确实花了不少心思,但用了不少冷门物料 + 大面积 0402 + 接口分配这几点,我认为是不太适合作为学习用板的。毕竟,你也不想 SSI 接口没有 CSN 信号给 MT6701 / 调试的时候要分别接电源、串口、SWD 要插在 5 个连接器/翻遍了淘宝才发现晶振有一家卖,结果封装还不是完全对应…的吧!
  1. 将 BOM 表打印出来,并将每种元件进行标号

    /posts/ctrl-foc-lite/bom.jpg
    BOM表

  2. 依照表格顺序,将每 10 种元件用胶带进行串联固定,即袋口朝向同一方向,标签正/反交替放置,然后用一条长胶带粘接在同一面。

    /posts/ctrl-foc-lite/materialChain.png
    元件链

  3. 将元件包进行折叠,然后用橡皮筋捆扎,放入盒中。

    /posts/ctrl-foc-lite/materialPile.jpg
    捆扎起来的元件
    /posts/ctrl-foc-lite/materialBox.jpg
    放在盒中的元件

  4. 对于不宜堆叠的大袋/盒装,单独放置。

  5. 检索元件时,先在 BOM 表上查找,定位到第 n 组的第 m 包,直接取出即可。

通过浏览硬件原理图,可以看到,这块板子的硬件电路大体可以分为核心系统、传感、通信、电源、驱动、显示和接口部分。下面对焊接顺序进行规划。

  • 电源部分。 电源是任何其他部分工作的必要条件,这部分先落实,其他部分才有可能进行测试和使用。
  • 核心系统。 同上。这个板子太小了,不太容易进行单元测试。所以选择直接用程序检查各部分的工作情况。
  • 驱动部分。 这部分是电机驱动的必要部分。
  • 传感部分。 这部分是实现电流闭环的必要部分。
  • 接口部分(还有按键等)。 这部分含有很大量的塑料部件。如果先贴这部分,有可能在使用风枪时把塑料部分吹化。所以放在最后焊。实测先在焊盘上 138℃ 低温焊锡,然后用 180℃ 左右的热风吹着定位 SH1.25 连接器,效果很好。
  • 显示部分。 这里单单指屏幕。相关的阻容应该放在前面焊好。又怕热、又怕压的屏幕不放在最后处理,想啥呢?
/posts/ctrl-foc-lite/schematicOverview.png
原理图概览
  • 电源部分贴装完成后,对各电源输出进行测量。确认电压正常。由于 TypeC 接口还没有贴装,从 PWM 输入接口( 2.54mm 排针)处的 5V 飞线输入。
    • 3.3V LDO
    • 5V DCDC
    • 2.5V 电压基准
/posts/ctrl-foc-lite/pwmPort.png
PWM输入接口
  • 每次焊接部分完成后,用放大镜辅助观察是否有虚焊引脚。
  • 核心系统焊接完成后,尝试烧录固件、设计程序读写串口。检查复位功能是否可用。
  • 驱动部分某些引脚敷铜面积比较大、散热快,比较容易虚焊,需要当心。
  • 长时间加热会导致阻焊层变黄、开裂,加热时在相关区域推一些焊油,风枪绕圈避免集中区域加热。我真的吹爆了一片板子

这里选用了 vscode + PlatformIO + Arduino Framework 的方案。创建一个 Arduino Framework、板信息为 Genetic STM32F103CB 的工程,然后把simpleFOC(库名为Arduino-FOC)、 Adafruit_BusIOAdafruit_SSD1306Adafruit-GFX-Library和其他用到的库克隆到lib文件夹里。
别用仓库里的代码直接跑,信我。 其实没差,但是要稍微搞一搞让 PlatformIO 识别到这个工程。

注意
PlatformIO 主页刷新不出来?无法拉取芯片/板库/代码?
你需要神秘的力量!

main.cpp 删删干净,只留下 setup()loop() 这两个基本函数在就可以了。此外,我们可以顺带测试一下串口和时钟正常不正常。

void setup()
{
    Serial.begin(115200);
    Serial.println("Serial init done.");
    delay(1000);
}

void loop()
{
    static int cnt = 0;
    cnt++;
    Serial.println(cnt);
    delay(1000);
}

接下来去配置烧录/调试设置。
我使用的是创芯工坊的 PW-Link2 ,算是 DAP-Link 的一种。
于是乎,打开 platformio.ini,追加/编辑以下项目:

upload_protocol = cmsis-dap
debug_tool = cmsis-dap

具体更改方式:

  • 若使用 ST-LINK,则无需更改。
  • 若使用 DAP-LINK,则将 upload_protocoldebug_tool 都改成 CMSIS-DAP
  • 若使用 J-LINK,则将 upload_protocoldebug_tool 都改成 jlink
  • (不会有人真的用 串口 给STM32烧录程序吧)
具体/其他请参考

在platformIO的插件菜单里,依次选择stm32f103cbbuildupload 。如果顺利,那么可以看到 success 字样,说明程序成功下到芯片中了。

/posts/ctrl-foc-lite/flashTest_log_sc.png
烧录测试-成功

如果顺利,在串口上看到开机时显示 init done。之后每秒钟刷新一条。按下复位键,可以重新观察到。

成功
那么,MCU基本系统就没有问题了~

本来是想用 MT6701 的,却发现这块板子的 SPI 接口没有提供 CSN 信号,遂作罢。具体现象就是只有开机的一瞬能读到位置,后面就不行了。(好像也可能是因为我没有重新获取)
此外,MT6701 需要去 Arduino-FOC-drivers 仓库找一下驱动,默认的 SPI sensor 是没法给他用的。
那就假装我们一开始就选的 AS5600 吧(

SimpleFOC 库本身即提供了 AS5600 的驱动程序,无需额外下载扩展。通过阅读源码可以得知,我们需用的 MagneticSensorI2C 类派生自 Sensor 类,而 AS5600 的特性是作为一个含有配置信息的结构体来传递的( MagneticSensorI2CConfig_s 类型)。AS5600 的配置信息存储在一个名为 AS5600_I2C 的常量中。

/*  file:lib\Arduino-FOC\src\sensors\MagneticSensorI2C.h */
struct MagneticSensorI2CConfig_s  {
  int chip_address;
  int bit_resolution;
  int angle_register;
  int data_start_bit; 
};

/*  file:lib\Arduino-FOC\src\sensors\MagneticSensorI2C.cpp */
MagneticSensorI2CConfig_s AS5600_I2C = {
  .chip_address = 0x36,
  .bit_resolution = 12,
  .angle_register = 0x0C,
  .data_start_bit = 11
};

要使用它,我们首先要创建一个 MagneticSensorI2C 对象。面向对象,爽啊! 使用的构造函数如下:

MagneticSensorI2C as5600 = MagneticSensorI2C(AS5600_I2C);

直接扔在全局里即可,不必放在函数里。
然后在初始化过程里使用 as5600.init() 函数,即可完成传感器的初始化。

注意
调用之前可记得把传感器对应的线插在板子上,电机轴上要贴 磁铁 啊!
SCL<–>PB6
SDA<–>PB7

然后在主循环中获取并打印出位置信息。

    // 从传感器获取到原始信息,后面将传感器连接至电机后不用这个函数获取。
    as5600.update();
    // 更新得到转子位置(弧度)并打印出来
    Serial.println(as5600.getAngle())
    delay(333);

可以观察到串口输出转子位置(弧度)。且正转、反转时可以观察到输出角度不断累积,转过一圈后读数没有从 6.28 归零,而是继续增加。
getAngle() 替换成 getRawAngle() 可以将结果约束在 06.28(2*PI)

成功
那么,磁编码器就没有问题了~

这块板子使用了一个 SSD1306 控制的,64x32 分辨率的OLED屏,并通过 I2C 接口进行连接。
值得注意的是,屏幕并没有与磁编码器共享同一条总线,而是一条单独的总线。

Arduino 这么一个勃勃生机万物竞发的生态中,当然要找现成的库用啦~ 这里我们选择了这些库:

  • Adafruit_SSD1306
  • Adafruit-GFX-Library
  • Adafruit_BusIO
    这三个库逐级调用,缺一不可。移植完成后,可以直接对显存进行操作,并进行图形绘制。

但是!由于 SSD1306 应用于 128x64128x32 更多一些,这套库没有直接对 64x32 的支持。所以我们要进行两处修改。

#1Adafruit_SSD1306 类的初始化函数 begin(uint8_t vcs, uint8_t addr, bool reset, bool periphBegin) 中,将这一段:

if ((WIDTH == 128) && (HEIGHT == 32)) {
    comPins = 0x02;
    contrast = 0x8F;
  } else if ((WIDTH == 128) && (HEIGHT == 64)) {
    comPins = 0x12;
    contrast = (vccstate == SSD1306_EXTERNALVCC) ? 0x9F : 0xCF;
  } else if ((WIDTH == 96) && (HEIGHT == 16)) {
    comPins = 0x2; // ada x12
    contrast = (vccstate == SSD1306_EXTERNALVCC) ? 0x10 : 0xAF;
  } else {
    // Other screen varieties -- TBD
  }

进行修改,在 TBD 注释的这一行中添加:

if ((WIDTH == 64) && (HEIGHT == 32)){
    comPins = 0x12;
    contrast = 0x4F;
}
为什么?
comPins 中修改的位描述了行线为 逐行地址隔行地址contrast 描述了屏幕输出的对比度。

#2Adafruit_SSD1306 类的显示绘制函数 display() 中,将这一段:

static const uint8_t PROGMEM dlist1[] = {
      SSD1306_PAGEADDR,
      0,                      // Page start address
      0xFF,                   // Page end (not really, but works here)
      SSD1306_COLUMNADDR, 0}; // Column start address
  ssd1306_commandList(dlist1, sizeof(dlist1));
  ssd1306_command1(WIDTH - 1); // Column end address

进行修改,仿照 dlist1 为 64 像素宽的屏幕单独做一套命令列表,为了区分,将两套分别命名为 dlist128dlist64 ;然后按照屏幕宽度使用不同的命令列表,注意 列结束地址 也发生了更改。

  static const uint8_t PROGMEM dlist128[] = {
      SSD1306_PAGEADDR,
      0,                      // Page start address
      0xFF,                   // Page end (not really, but works here)
      SSD1306_COLUMNADDR, 0}; // Column start address

  static const uint8_t PROGMEM dlist64[] = {
      SSD1306_PAGEADDR,
      0,                      // Page start address
      0xFF,                   // Page end (not really, but works here)
      SSD1306_COLUMNADDR, 0x20}; // Column start addressF
  if(WIDTH ==128)
  {
    ssd1306_commandList(dlist128, sizeof(dlist128));
    ssd1306_command1(WIDTH - 1); // Column end address
  }else
  if(WIDTH == 64)
  {
    ssd1306_commandList(dlist64, sizeof(dlist64));
    ssd1306_command1(WIDTH - 1 + 0x20); // Column end address
  }
为什么?
SSD1306 提供了 128 条列线,因此最大支持 128 像素宽的屏幕。
然而,对于 64 像素宽的屏幕而言,并没有从地址 0x00 使用 SSD1306,而是采取了中央对齐,即使用了中间的 0x200x5F 的范围。
具体目的不详,也许是为了均衡不同部分的供电,或者出于逻辑优化角度考虑。

调这个玩意足足花了一天半的时间 (;´д`)ゞ

为了便于后续参考和调试,在 src\ 下新建 custom_graph.ccustom_graph.h 文件。内容如下(演译自官方样例):

/*custom_graph.h*/
#ifndef __CUSTOM_GRAPH_H__
#define __CUSTOM_GRAPH_H__

#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

extern Adafruit_SSD1306 display;

void bsp_screen_init(void);

void screen_show_val(const char* name,int a);
void screen_show_val(const char* name,float a);

#endif // __CUSTOM_GRAPH_H__
/*custom_graph.c*/
#include "custom_graph.h"

// void bsp_screen_init(void)

#define SCREEN_WIDTH 64 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
// The pins for I2C are defined by the Wire-library.
// On an arduino UNO:       A4(SDA), A5(SCL)
// On an arduino MEGA 2560: 20(SDA), 21(SCL)
// On an arduino LEONARDO:   2(SDA),  3(SCL), ...
#define OLED_RESET -1       // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
TwoWire Wire_Oled(PB11, PB10);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire_Oled, OLED_RESET);

#define NUMFLAKES 10 // Number of snowflakes in the animation example

#define LOGO_HEIGHT 16
#define LOGO_WIDTH 16
static const unsigned char PROGMEM logo_bmp[] =
    {0b00000000, 0b11000000,
     0b00000001, 0b11000000,
     0b00000001, 0b11000000,
     0b00000011, 0b11100000,
     0b11110011, 0b11100000,
     0b11111110, 0b11111000,
     0b01111110, 0b11111111,
     0b00110011, 0b10011111,
     0b00011111, 0b11111100,
     0b00001101, 0b01110000,
     0b00011011, 0b10100000,
     0b00111111, 0b11100000,
     0b00111111, 0b11110000,
     0b01111100, 0b11110000,
     0b01110000, 0b01110000,
     0b00000000, 0b00110000};

void testdrawline()
{
    int16_t i;

    display.clearDisplay(); // Clear display buffer

    for (i = 0; i < display.width(); i += 4)
    {
        display.drawLine(0, 0, i, display.height() - 1, SSD1306_WHITE);
        display.display(); // Update screen with each newly-drawn line
        delay(1);
    }
    for (i = 0; i < display.height(); i += 4)
    {
        display.drawLine(0, 0, display.width() - 1, i, SSD1306_WHITE);
        display.display();
        delay(1);
    }
    delay(250);

    display.clearDisplay();

    for (i = 0; i < display.width(); i += 4)
    {
        display.drawLine(0, display.height() - 1, i, 0, SSD1306_WHITE);
        display.display();
        delay(1);
    }
    for (i = display.height() - 1; i >= 0; i -= 4)
    {
        display.drawLine(0, display.height() - 1, display.width() - 1, i, SSD1306_WHITE);
        display.display();
        delay(1);
    }
    delay(250);

    display.clearDisplay();

    for (i = display.width() - 1; i >= 0; i -= 4)
    {
        display.drawLine(display.width() - 1, display.height() - 1, i, 0, SSD1306_WHITE);
        display.display();
        delay(1);
    }
    for (i = display.height() - 1; i >= 0; i -= 4)
    {
        display.drawLine(display.width() - 1, display.height() - 1, 0, i, SSD1306_WHITE);
        display.display();
        delay(1);
    }
    delay(250);

    display.clearDisplay();

    for (i = 0; i < display.height(); i += 4)
    {
        display.drawLine(display.width() - 1, 0, 0, i, SSD1306_WHITE);
        display.display();
        delay(1);
    }
    for (i = 0; i < display.width(); i += 4)
    {
        display.drawLine(display.width() - 1, 0, i, display.height() - 1, SSD1306_WHITE);
        display.display();
        delay(1);
    }

    delay(2000); // Pause for 2 seconds
}

void testdrawrect(void)
{
    display.clearDisplay();

    for (int16_t i = 0; i < display.height() / 2; i += 2)
    {
        display.drawRect(i, i, display.width() - 2 * i, display.height() - 2 * i, SSD1306_WHITE);
        display.display(); // Update screen with each newly-drawn rectangle
        delay(1);
    }

    delay(2000);
}

void testfillrect(void)
{
    display.clearDisplay();

    for (int16_t i = 0; i < display.height() / 2; i += 3)
    {
        // The INVERSE color is used so rectangles alternate white/black
        display.fillRect(i, i, display.width() - i * 2, display.height() - i * 2, SSD1306_INVERSE);
        display.display(); // Update screen with each newly-drawn rectangle
        delay(1);
    }

    delay(2000);
}

void testdrawcircle(void)
{
    display.clearDisplay();

    for (int16_t i = 0; i < max(display.width(), display.height()) / 2; i += 2)
    {
        display.drawCircle(display.width() / 2, display.height() / 2, i, SSD1306_WHITE);
        display.display();
        delay(1);
    }

    delay(2000);
}

void testfillcircle(void)
{
    display.clearDisplay();

    for (int16_t i = max(display.width(), display.height()) / 2; i > 0; i -= 3)
    {
        // The INVERSE color is used so circles alternate white/black
        display.fillCircle(display.width() / 2, display.height() / 2, i, SSD1306_INVERSE);
        display.display(); // Update screen with each newly-drawn circle
        delay(1);
    }

    delay(2000);
}

void testdrawroundrect(void)
{
    display.clearDisplay();

    for (int16_t i = 0; i < display.height() / 2 - 2; i += 2)
    {
        display.drawRoundRect(i, i, display.width() - 2 * i, display.height() - 2 * i,
                              display.height() / 4, SSD1306_WHITE);
        display.display();
        delay(1);
    }

    delay(2000);
}

void testfillroundrect(void)
{
    display.clearDisplay();

    for (int16_t i = 0; i < display.height() / 2 - 2; i += 2)
    {
        // The INVERSE color is used so round-rects alternate white/black
        display.fillRoundRect(i, i, display.width() - 2 * i, display.height() - 2 * i,
                              display.height() / 4, SSD1306_INVERSE);
        display.display();
        delay(1);
    }

    delay(2000);
}

void testdrawtriangle(void)
{
    display.clearDisplay();

    for (int16_t i = 0; i < max(display.width(), display.height()) / 2; i += 5)
    {
        display.drawTriangle(
            display.width() / 2, display.height() / 2 - i,
            display.width() / 2 - i, display.height() / 2 + i,
            display.width() / 2 + i, display.height() / 2 + i, SSD1306_WHITE);
        display.display();
        delay(1);
    }

    delay(2000);
}

void testfilltriangle(void)
{
    display.clearDisplay();

    for (int16_t i = max(display.width(), display.height()) / 2; i > 0; i -= 5)
    {
        // The INVERSE color is used so triangles alternate white/black
        display.fillTriangle(
            display.width() / 2, display.height() / 2 - i,
            display.width() / 2 - i, display.height() / 2 + i,
            display.width() / 2 + i, display.height() / 2 + i, SSD1306_INVERSE);
        display.display();
        delay(1);
    }

    delay(2000);
}

void testdrawchar(void)
{
    display.clearDisplay();

    display.setTextSize(1);              // Normal 1:1 pixel scale
    display.setTextColor(SSD1306_WHITE); // Draw white text
    display.setCursor(0, 0);             // Start at top-left corner
    display.cp437(true);                 // Use full 256 char 'Code Page 437' font

    // Not all the characters will fit on the display. This is normal.
    // Library will draw what it can and the rest will be clipped.
    for (int16_t i = 0; i < 256; i++)
    {
        if (i == '\n')
            display.write(' ');
        else
            display.write(i);
    }

    display.display();
    delay(2000);
}

void testdrawstyles(void)
{
    display.clearDisplay();

    display.setTextSize(1);              // Normal 1:1 pixel scale
    display.setTextColor(SSD1306_WHITE); // Draw white text
    display.setCursor(0, 0);             // Start at top-left corner
    display.println(F("Hello, world!"));

    display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Draw 'inverse' text
    display.println(3.141592);

    display.setTextSize(2); // Draw 2X-scale text
    display.setTextColor(SSD1306_WHITE);
    display.print(F("0x"));
    display.println(0xDEADBEEF, HEX);

    display.display();
    delay(2000);
}

void testscrolltext(void)
{
    display.clearDisplay();

    display.setTextSize(2); // Draw 2X-scale text
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(10, 0);
    display.println(F("scroll"));
    display.display(); // Show initial text
    delay(100);

    // Scroll in various directions, pausing in-between:
    display.startscrollright(0x00, 0x0F);
    delay(2000);
    display.stopscroll();
    delay(1000);
    display.startscrollleft(0x00, 0x0F);
    delay(2000);
    display.stopscroll();
    delay(1000);
    display.startscrolldiagright(0x00, 0x07);
    delay(2000);
    display.startscrolldiagleft(0x00, 0x07);
    delay(2000);
    display.stopscroll();
    delay(1000);
}

void testdrawbitmap(void)
{
    display.clearDisplay();

    display.drawBitmap(
        (display.width() - LOGO_WIDTH) / 2,
        (display.height() - LOGO_HEIGHT) / 2,
        logo_bmp, LOGO_WIDTH, LOGO_HEIGHT, 1);
    display.display();
    delay(1000);
}

#define XPOS 0 // Indexes into the 'icons' array in function below
#define YPOS 1
#define DELTAY 2

void testanimate(const uint8_t *bitmap, uint8_t w, uint8_t h)
{
    int8_t f, icons[NUMFLAKES][3];

    // Initialize 'snowflake' positions
    for (f = 0; f < NUMFLAKES; f++)
    {
        icons[f][XPOS] = random(1 - LOGO_WIDTH, display.width());
        icons[f][YPOS] = -LOGO_HEIGHT;
        icons[f][DELTAY] = random(1, 6);
        Serial.print(F("x: "));
        Serial.print(icons[f][XPOS], DEC);
        Serial.print(F(" y: "));
        Serial.print(icons[f][YPOS], DEC);
        Serial.print(F(" dy: "));
        Serial.println(icons[f][DELTAY], DEC);
    }

    for (;;)
    {                           // Loop forever...
        display.clearDisplay(); // Clear the display buffer

        // Draw each snowflake:
        for (f = 0; f < NUMFLAKES; f++)
        {
            display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SSD1306_WHITE);
        }

        display.display(); // Show the display buffer on the screen
        delay(200);        // Pause for 1/10 second

        // Then update coordinates of each flake...
        for (f = 0; f < NUMFLAKES; f++)
        {
            icons[f][YPOS] += icons[f][DELTAY];
            // If snowflake is off the bottom of the screen...
            if (icons[f][YPOS] >= display.height())
            {
                // Reinitialize to a random position, just off the top
                icons[f][XPOS] = random(1 - LOGO_WIDTH, display.width());
                icons[f][YPOS] = -LOGO_HEIGHT;
                icons[f][DELTAY] = random(1, 6);
            }
        }
    }
}

void bsp_screen_init()
{
    //Serial.begin(9600);

    // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
    if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS))
    {
        Serial.println(F("SSD1306 allocation failed"));
        for (;;)
            ; // Don't proceed, loop forever
    }
    Serial.println("SSD1306 allocation success");

    // Show initial display buffer contents on the screen --
    // the library initializes this with an Adafruit splash screen.
    display.display();
    delay(2000); // Pause for 2 seconds

    // Clear the buffer
    display.clearDisplay();

    
}


void screen_show_val(const char* name,int a)
{
    //display.clearDisplay();
    display.setTextSize(1); // Draw 2X-scale text
    display.setTextColor(SSD1306_WHITE);
    display.printf(F("%s=%d"),name,a);
    //display.display(); // Show initial text
}

void screen_show_val(const char* name,float a)
{
    //display.clearDisplay();
    display.setTextSize(1); // Draw 2X-scale text
    display.setTextColor(SSD1306_WHITE);
    if(a>=0.0f)
    {
        display.printf(F("%s=%d.%d"),name,(int)a,(int)(a*100)%100);
    }else
    {
        a*=-1.0f;
        display.printf(F("%s=-%d.%d"),name,(int)a,(int)(a*100)%100);
    }
    //display.display(); // Show initial text
}

概括而言,就是把样例中的 setup() 入口函数更名为 bsp_screen_init() ,然后为了方便在屏幕上显示调试信息设计了 screen_show_val 函数来把数字显示在屏幕上。
整个文件实现的功能是为屏幕重新定义了一条 I2C 总线( PB10、 PB11 ),并命名为 Wire_Oled 。然后构造了一个 Adafruit_SSD1306 类的对象 display,并定义了它的尺寸、总线和复位引脚。

在初始化中调用 bsp_screen_init() ,就可以看到 Adafruit 提供的图形绘制示例了。

/posts/ctrl-foc-lite/screenDemo.jpg
正在显示图形动画演示
成功
那么,屏幕显示就没有问题了~

板上的驱动使用了一颗 DRV8313 ,内置了 3 个半桥的 MOSFET ,最大持续过流能力约 1A 。
由原理图可知,驱动的 A B C 三相信号分别接在了 PB4 PB5 PB0,并且驱动的使能信号接在了 PB12 ,高电平有效。

使用 Arduino 提供的函数,测试驱动输出功能正常。

测试条件
测试过程中,在电源输入处接入了 12V 电压。
void setup()
{
    pinMode(PB_12,OUTPUT);
    pinMode(PB_4,OUTPUT);
    pinMode(PB_5,OUTPUT);
    pinMode(PB_0,OUTPUT);
    digitalWrite(PB_12,HIGH);
}

void loop()
{
    analogWrite(PB_4, 64);      //val can be 0-255
    analogWrite(PB_5, 128);
    analogWrite(PB_0, 192);
}

输出值最大为 255 ,即 A B C 相的输出占空比分别被设置为 25% 50% 75%

测量输出电压,分别接近 3V 6V 9V ,符合预期。

成功
那么,驱动硬件部分 就没有问题了~
咦,为什么这么说?

Ctrl-FOC-lite 采用了 2 个 INA240 读取 A B 相的 在线电流 。由基尔霍夫电流定律可得,三相电流的代数和为 0 ,则 C 相电流即为其余两相电流之和的 相反数 。定义电流从电机中性点流出为正方向。

查找数据手册可得,INA240A2 的增益为50V/V, 采样电阻(Shunt) 为 0.01R。INA240 可视为一个仪表放大器,则每 1A 电流在 stm32 的模拟输入引脚上产生 500mV 电压。

电流传感器的驱动程序由 SimpleFOC 提供。由于最终输入为模拟信号,所以此类型与所选传感器型号区别不大,通过调整参数均可适配。

通过阅读源码得知,我们需构造一个 InlineCurrentSense 类的对象,此类派生自 CurrentSense 类,其构造函数如下:

InlineCurrentSense  current_sense = InlineCurrentSense(0.01f,50.0f, A0, A1, NOT_SET);
//参数依次为:采样电阻大小、放大器增益、A相输入引脚、B相输入引脚、C相输入引脚
注意
这里一定要注意,放大器增益要以浮点数形式,并且把没有采集的相位设置为 NOT_SET
如果将放大器增益写为整数,则会被编译器识别为构造函数的另一个重载函数,导致无法正常初始化。

其余测试程序如下:

float i_a,i_b,i_c;

void setup(){
    current_sense.init();   //初始化电流传感器
    Serial.begin(115200);   
}

void loop()
{
    PhaseCurrent_s currents = current_sense.getPhaseCurrents();     //获取三相电流,current.a/current.b/current.c为float类型
    float current_magnitude = current_sense.getDCCurrent();         //获取直流电流幅度(总线电流)

    //为方便观察换算为mA
    i_a = currents.a * 1000.0f;
    i_b = currents.b * 1000.0f;
    i_c = - i_a - i_b;
    Serial.write(F("%d,%d,%d\n"),(int)i_a,(int)i_b,(int)i_c);
    delay(333);
}

同期配合上面上面驱动输出一点点电压( A = 1V, B = 0V, C = 0V ),此时读到 A相 电流为负,且其绝对值约为 B相 的两倍。
如果频率不是很高,电感不是很大,可能会看到电流读数有较大的跳动。

成功
那么,电流传感器 就没有问题了~
无理も无理
手边暂时没有CAN总线的测试条件,加入TODO
参考文档

参照 simpleFOC 提供的驱动测试样例程序 bldc_driver_3pwm_standalone.ino ,设计了一个初始化驱动并输出 3V 6V 9V 的测试程序,发现 B相 输出电压不能达到预期,而是常为高电平, 即输出 12V。 在笔者顺着 simpleFOC 及其对 stm32 的适配程序定位故障时,发生电源反灌导致 ctrl-foc-lite 板损坏,电源芯片发烫。PW-LINK2 还能被电脑识别,但出现异常发热。

复刻过程暂告一段落,待重新设计硬件后继续。

你知道的太多了