Ctrl-FOC-Lite复刻记录
后来者请谨慎入坑。
>/0 前言
基于磁场定向控制(FOC)的电机驱动技术已成为机电系统高精度运动控制领域的核心研究方向之一。本文立足于对开源项目 Ctrl-FOC-Lite 的复刻实践,对无刷电机控制技术进行直观的学习、理解和应用。
想当初,我以为只是让电机转起来很简单,用基于 Arduino 框架的 SimpleFOC 就更简单了。但事实表明,我在这里花了远多于预期的时间,太!菜!了! 因此写了本文记录过程和相关启发与思考。
(遇到的问题中,相当多的部分是由于环境的抽象程度太高 + stm32 不是原生平台,难免遇到一些兼容性问题)
但是有一说一, C++ 写起来确实比C爽
>/1 焊接过程
物料归类方法
-
依照表格顺序,将每 10 种元件用胶带进行串联固定,即袋口朝向同一方向,标签正/反交替放置,然后用一条长胶带粘接在同一面。
元件链 -
对于不宜堆叠的大袋/盒装,单独放置。
-
检索元件时,先在 BOM 表上查找,定位到第
n
组的第m
包,直接取出即可。
组成分析
通过浏览硬件原理图,可以看到,这块板子的硬件电路大体可以分为核心系统、传感、通信、电源、驱动、显示和接口部分。下面对焊接顺序进行规划。
- 电源部分。 电源是任何其他部分工作的必要条件,这部分先落实,其他部分才有可能进行测试和使用。
- 核心系统。 同上。这个板子太小了,不太容易进行单元测试。所以选择直接用程序检查各部分的工作情况。
- 驱动部分。 这部分是电机驱动的必要部分。
- 传感部分。 这部分是实现电流闭环的必要部分。
- 接口部分(还有按键等)。 这部分含有很大量的塑料部件。如果先贴这部分,有可能在使用风枪时把塑料部分吹化。所以放在最后焊。实测先在焊盘上 138℃ 低温焊锡,然后用 180℃ 左右的热风吹着定位 SH1.25 连接器,效果很好。
- 显示部分。 这里单单指屏幕。相关的阻容应该放在前面焊好。又怕热、又怕压的屏幕不放在最后处理,想啥呢?

初步测试
- 电源部分贴装完成后,对各电源输出进行测量。确认电压正常。由于 TypeC 接口还没有贴装,从 PWM 输入接口(
2.54mm
排针)处的5V
飞线输入。- 3.3V LDO
- 5V DCDC
- 2.5V 电压基准

- 每次焊接部分完成后,用放大镜辅助观察是否有虚焊引脚。
- 核心系统焊接完成后,尝试烧录固件、设计程序读写串口。检查复位功能是否可用。
- 驱动部分某些引脚敷铜面积比较大、散热快,比较容易虚焊,需要当心。
- 长时间加热会导致阻焊层变黄、开裂,加热时在相关区域推一些焊油,风枪绕圈避免集中区域加热。我真的吹爆了一片板子
>/2 功能测试
SDK环境部署 & MCU基本系统测试 & 串口测试
这里选用了 vscode + PlatformIO + Arduino Framework 的方案。创建一个 Arduino Framework、板信息为 Genetic STM32F103CB 的工程,然后把simpleFOC(库名为Arduino-FOC
)、 Adafruit_BusIO
、 Adafruit_SSD1306
、 Adafruit-GFX-Library
和其他用到的库克隆到lib文件夹里。
别用仓库里的代码直接跑,信我。
其实没差,但是要稍微搞一搞让 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_protocol
和debug_tool
都改成CMSIS-DAP
- 若使用
J-LINK
,则将upload_protocol
和debug_tool
都改成jlink
- (不会有人真的用
串口
给STM32烧录程序吧)
在platformIO的插件菜单里,依次选择stm32f103cb
的 build
和 upload
。如果顺利,那么可以看到 success 字样,说明程序成功下到芯片中了。

如果顺利,在串口上看到开机时显示 init done
。之后每秒钟刷新一条。按下复位键,可以重新观察到。
磁编码器
本来是想用 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()
可以将结果约束在 0
到 6.28(2*PI)
。
显示
这块板子使用了一个 SSD1306
控制的,64x32
分辨率的OLED屏,并通过 I2C
接口进行连接。
值得注意的是,屏幕并没有与磁编码器共享同一条总线,而是一条单独的总线。
在 Arduino
这么一个勃勃生机万物竞发的生态中,当然要找现成的库用啦~ 这里我们选择了这些库:
Adafruit_SSD1306
Adafruit-GFX-Library
Adafruit_BusIO
这三个库逐级调用,缺一不可。移植完成后,可以直接对显存进行操作,并进行图形绘制。
但是!由于 SSD1306
应用于 128x64
、 128x32
更多一些,这套库没有直接对 64x32
的支持。所以我们要进行两处修改。
#1 在 Adafruit_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
描述了屏幕输出的对比度。#2 在 Adafruit_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 像素宽的屏幕单独做一套命令列表,为了区分,将两套分别命名为 dlist128
和 dlist64
;然后按照屏幕宽度使用不同的命令列表,注意 列结束地址 也发生了更改。
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
,而是采取了中央对齐,即使用了中间的 0x20
至 0x5F
的范围。具体目的不详,也许是为了均衡不同部分的供电,或者出于逻辑优化角度考虑。
调这个玩意足足花了一天半的时间 (;´д`)ゞ
为了便于后续参考和调试,在 src\
下新建 custom_graph.c
和 custom_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 提供的图形绘制示例了。

驱动输出
板上的驱动使用了一颗 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总线
>/3 SimpleFOC
SimpleFOC官方中文文档
驱动测试
参照 simpleFOC 提供的驱动测试样例程序 bldc_driver_3pwm_standalone.ino
,设计了一个初始化驱动并输出 3V
6V
9V
的测试程序,发现 B相
输出电压不能达到预期,而是常为高电平,
即输出 12V
。 在笔者顺着 simpleFOC 及其对 stm32 的适配程序定位故障时,发生电源反灌导致 ctrl-foc-lite
板损坏,电源芯片发烫。PW-LINK2
还能被电脑识别,但出现异常发热。
复刻过程暂告一段落,待重新设计硬件后继续。
你知道的太多了