SimpleFOC源码学习09(v2.3.2) - 磁编码器MagneticSensorSPI.cpp与MagneticSensorSPI.h

导言


github 源码:

但如果换成 SPI 磁编码器,问题就完全变了:

  • 为什么同样是"读位置",这里不再是查表,而是读一颗专用芯片的寄存器?
  • 为什么看起来只是读一个角度值,源码里却要处理 奇偶校验、两帧传输、位对齐、掩码
  • 它最终又是怎么接回 Sensor 基类,继续复用多圈角度和速度计算这套通用逻辑的?

这篇就来拆 MagneticSensorSPI.cpp/.h。如果说 HallSensor 代表的是"分辨率低,但结构简单 ",那么 MagneticSensorSPI 代表的就是"分辨率高,但通信协议更复杂"。

先说结论:MagneticSensorSPI 的核心并不是"读一个 SPI 寄存器"这么简单,而是按不同芯片的 SPI 协议,正确拿到单圈机械角度,再交给 Sensor 基类做连续角度和速度管理

我们按以下五步来拆解:

一、硬件原理 - 这颗芯片在测什么?


MagneticSensorSPI 并不绑定某一款芯片,但它的默认配置是为 AMS AS5147/AS5047/AS5048 设计的。理解芯片工作原理,才能看懂那些"魔数"(0x3FFFbit 14、奇偶校验)从何而来。

关键点总结:

  1. 这类磁编码器通过芯片内部的磁场感应与角度解算电路,把转子当前位置转换成寄存器中的绝对角度值。对 MagneticSensorSPI 来说,源码真正关心的不是芯片内部模拟细节,而是:最终能不能按协议把这个角度寄存器读出来。
  2. 对这类 AMS 传感器来说,SPI 一次传输通常是 16-bit 帧 。但要注意:发送给芯片的命令帧芯片返回的数据帧 ,高位字段含义并不完全一样。命令帧里会带上 R/W 与奇偶校验;返回帧里高位通常是校验/错误标志,低 14 位才是角度数据。这正是后面 read() 函数要做位移和掩码的根本原因。
  3. 在 AS5147 这类器件以及 SimpleFOC 当前实现里,读取角度通常表现为:先发读命令,再在下一帧取回结果。所以代码里会看到两次 16-bit SPI 传输。你可以把它理解成:第一帧是在"发请求",第二帧才是在"取结果"。

二、配置结构体


MagneticSensorSPI.h

cpp 复制代码
struct MagneticSensorSPIConfig_s  {
  int spi_mode;
  long clock_speed;
  int bit_resolution;
  int angle_register;
  int data_start_bit;
  int command_rw_bit;
  int command_parity_bit;
};

MagneticSensorSPI.cpp

cpp 复制代码
/** Typical configuration for the 14bit AMS AS5147 magnetic sensor over SPI interface */
MagneticSensorSPIConfig_s AS5147_SPI = {
  .spi_mode = SPI_MODE1,
  .clock_speed = 1000000,
  .bit_resolution = 14,
  .angle_register = 0x3FFF,
  .data_start_bit = 13,
  .command_rw_bit = 14,
  .command_parity_bit = 15
};
// AS5048 and AS5047 are the same as AS5147
MagneticSensorSPIConfig_s AS5048_SPI = AS5147_SPI;
MagneticSensorSPIConfig_s AS5047_SPI = AS5147_SPI;

/** Typical configuration for the 14bit MonolithicPower MA730 magnetic sensor over SPI interface */
MagneticSensorSPIConfig_s MA730_SPI = {
  .spi_mode = SPI_MODE0,
  .clock_speed = 1000000,
  .bit_resolution = 14,
  .angle_register = 0x0000,
  .data_start_bit = 15,
  .command_rw_bit = 0,  // not required
  .command_parity_bit = 0 // parity not implemented
};

这一层配置结构体非常关键,因为 MagneticSensorSPI 并不是只服务 AS5147 一种芯片。它的思路是:把"芯片协议差异"参数化,再用同一套驱动流程去读取角度

也就是说,后面真正的主逻辑不是"为某个型号硬编码",而是"读取配置 -> 组织 SPI 帧 -> 提取有效位"。

三、两种构造函数


MagneticSensorSPI.h

cpp 复制代码
class MagneticSensorSPI: public Sensor{
 public:
    /**
     *  MagneticSensorSPI class constructor
     * @param cs  SPI chip select pin 
     * @param bit_resolution   sensor resolution bit number
     * @param angle_register  (optional) angle read register - default 0x3FFF
     */
    MagneticSensorSPI(int cs, int bit_resolution, int angle_register = 0);
    /**
     *  MagneticSensorSPI class constructor
     * @param config   SPI config
     * @param cs  SPI chip select pin
     */
    MagneticSensorSPI(MagneticSensorSPIConfig_s config, int cs);
    ....

MagneticSensorSPI.cpp

cpp 复制代码
// MagneticSensorSPI(int cs, float _bit_resolution, int _angle_register)
//  cs              - SPI chip select pin
//  _bit_resolution   sensor resolution bit number
// _angle_register  - (optional) angle read register - default 0x3FFF
MagneticSensorSPI::MagneticSensorSPI(int cs, int _bit_resolution, int _angle_register){

  chip_select_pin = cs;
  // angle read register of the magnetic sensor
  angle_register = _angle_register ? _angle_register : DEF_ANGLE_REGISTER;
  // register maximum value (counts per revolution)
  cpr = _powtwo(_bit_resolution);
  spi_mode = SPI_MODE1;
  clock_speed = 1000000;
  bit_resolution = _bit_resolution;

  command_parity_bit = 15; // for backwards compatibilty
  command_rw_bit = 14; // for backwards compatibilty
  data_start_bit = 13; // for backwards compatibilty
}

MagneticSensorSPI::MagneticSensorSPI(MagneticSensorSPIConfig_s config, int cs){
  chip_select_pin = cs;
  // angle read register of the magnetic sensor
  angle_register = config.angle_register ? config.angle_register : DEF_ANGLE_REGISTER;
  // register maximum value (counts per revolution)
  cpr = _powtwo(config.bit_resolution);
  spi_mode = config.spi_mode;
  clock_speed = config.clock_speed;
  bit_resolution = config.bit_resolution;

  command_parity_bit = config.command_parity_bit; // for backwards compatibilty
  command_rw_bit = config.command_rw_bit; // for backwards compatibilty
  data_start_bit = config.data_start_bit; // for backwards compatibilty
}


_powtwo(14)foc_utils.h 里一个纯整数的 1 << n,避免浮点误差。cpr(counts per revolution)是后续所有角度换算的基准,可以理解为"一圈被分成多少份"。
MA730_SPIcommand_rw_bit=0command_parity_bit=0 说明 MA730 这类配置不需要这两个控制位。也就是说,它的命令帧更简单;但 read() 仍然沿用同一套两帧事务、CS 拉高拉低和位对齐流程,if (command_rw_bit > 0) 判断正是为此而设。

这里可以顺手看懂这两个构造函数的分工:

  • MagneticSensorSPI(int cs, int bit_resolution, int angle_register = 0):适合快速上手,默认按兼容 AMS 系列的方式工作
  • MagneticSensorSPI(MagneticSensorSPIConfig_s config, int cs):适合协议不完全一样的芯片,直接把 SPI 模式、寄存器地址、有效数据起始位、校验位等都显式传进来

这和前面几篇的风格是一致的:SimpleFOC 总是先给你一个"够用的默认实现",再保留面向不同硬件扩展的入口。

四、init() 做了什么?


很多人读到构造函数会以为对象已经"能用了",但对 MagneticSensorSPI 来说,还差一步 init()

cpp 复制代码
void MagneticSensorSPI::init(SPIClass* _spi){
  spi = _spi;
  settings = SPISettings(clock_speed, MSBFIRST, spi_mode);
  pinMode(chip_select_pin, OUTPUT);
  spi->begin();
  digitalWrite(chip_select_pin, HIGH);
  this->Sensor::init();
}

这一段实际上完成了 4 件事:

  1. 保存 SPI 外设对象指针
  2. 根据时钟、位序、模式生成 SPISettings
  3. 初始化片选脚,并先拉高 CS
  4. 调用 Sensor::init(),完成基类状态初始化

最后这一行很容易被忽略,但它非常重要。因为 MagneticSensorSPI 自己只负责"拿到单圈机械角度",而连续角度、整圈计数、速度初始状态这些通用逻辑,仍然属于 Sensor 基类。

五、SPI通讯核心 - read()函数


这是整个文件里最值得反复看的部分。

cpp 复制代码
  /*
  * Read a register from the sensor
  * Takes the address of the register as a 16 bit word
  * Returns the value of the register
  */
word MagneticSensorSPI::read(word angle_register){
  word command = angle_register;

  if (command_rw_bit > 0) {
    command = angle_register | (1 << command_rw_bit);
  }
  if (command_parity_bit > 0) {
   	//Add a parity bit on the the MSB
  	command |= ((word)spiCalcEvenParity(command) << command_parity_bit);
  }

  //SPI - begin transaction
  spi->beginTransaction(settings);

  //Send the command
  digitalWrite(chip_select_pin, LOW);
  spi->transfer16(command);
  digitalWrite(chip_select_pin,HIGH);
  
#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) // if ESP32 board
  delayMicroseconds(50); // why do we need to delay 50us on ESP32? In my experience no extra delays are needed, on any of the architectures I've tested...
#else
  delayMicroseconds(1); // delay 1us, the minimum time possible in plain arduino. 350ns is the required time for AMS sensors, 80ns for MA730, MA702
#endif

  //Now read the response
  digitalWrite(chip_select_pin, LOW);
  word register_value = spi->transfer16(0x00);
  digitalWrite(chip_select_pin, HIGH);

  //SPI - end transaction
  spi->endTransaction();

  register_value = register_value >> (1 + data_start_bit - bit_resolution);  //this should shift data to the rightmost bits of the word

  const static word data_mask = 0xFFFF >> (16 - bit_resolution);
  return register_value & data_mask;  // Return the data, stripping the non data (e.g parity) bits
}

先按执行顺序,把它翻译成人话:

  1. 先根据芯片协议,拼出一帧 command
  2. 如果该芯片需要 R/W 位,就把"读寄存器"标志写进去
  3. 如果该芯片需要奇偶校验位,就把 parity 也补进去
  4. 拉低 CS,发送第一帧命令
  5. 拉高 CS,等待器件准备返回数据
  6. 再次拉低 CS,发送空数据,同时把上一帧请求对应的返回值读出来
  7. 对返回值做右移和掩码,只保留角度有效位

这里有两个容易误解的点。

第一个点:这不是"连续发 32bit",而是两个独立 SPI 帧。

每一帧都用 CS 的拉低/拉高包起来,告诉从设备"这是一笔完整事务"。所以这里的重点不是 transfer16() 调了两次,而是两次 transfer16() 之间的时序边界是被 CS 明确切开的

第二个点:read() 的核心不是"读寄存器",而是"按协议拿到上一帧返回的数据"。

如果你在 STM32 上自己移植类似驱动,最容易踩的坑往往不是角度换算,而是:

  • SPI mode 设错
  • CS 时序不符合器件要求
  • 命令帧的 R/W 或 parity 没拼对
  • 把返回帧的高位标志位误当成角度数据

现在专门把第⑥步的位操作展开说清楚,这是最容易让人困惑的地方:

cpp 复制代码
register_value = register_value >> (1 + data_start_bit - bit_resolution);
// AS5147: >> (1 + 13 - 14) = >> 0  → 不移位(数据正好在低14位)

const static word data_mask = 0xFFFF >> (16 - bit_resolution);
// AS5147: 0xFFFF >> (16 - 14) = 0xFFFF >> 2 = 0x3FFF  → 低14位全1的掩码

return register_value & data_mask;
// 清除高位协议字段,只保留14位角度数据

这套 (1 + data_start_bit - bit_resolution) 公式是为了兼容不同芯片------有些芯片数据不是从 bit0 开始的,需要先右移对齐再掩码。对 AS5147 而言位移量恰好是 0,但对其他芯片就不一定了。

再拿 AS5147 代入一遍,读者会更有感觉:

  • bit_resolution = 14
  • cpr = 1 << 14 = 16384
  • 如果原始计数 raw = 8192
  • 那么对应角度就是 8192 / 16384 * 2π = π rad

也就是说,原始计数在中点时,转子正好处在半圈位置。 还有一个源码层面的细节,值得顺手提一下:

cpp 复制代码
const static word data_mask = 0xFFFF >> (16 - bit_resolution);

这里把 data_mask 写成了函数内 static,这不只是"写法风格"问题,而是一个值得注意的潜在正确性风险 。因为它会在第一次调用时按当时的 bit_resolution 初始化一次;如果同一程序里混用不同 bit_resolutionMagneticSensorSPI 对象,后续实例会复用第一次生成的掩码,理论上就可能出现错误读数。

如果你后面自己写类似驱动,更稳妥的写法通常是把它改成普通局部变量,或者放到对象成员里按实例管理。

六、角度换算------从原始计数到弧度


cpp 复制代码
// 第一层:原始计数
int MagneticSensorSPI::getRawCount() {
    return (int)MagneticSensorSPI::read(angle_register);
}

// 第二层:换算成弧度 [0, 2π]
float MagneticSensorSPI::getSensorAngle() {
    return (getRawCount() / (float)cpr) * _2PI;
}


getSensorAngle() 返回的是 [0, 2π) 范围内的圈内机械角度 。注意这里仍然只是单圈绝对位置,还不是控制器最终使用的连续角度。

后面的多圈累积和速度计算,仍然完全交给基类 Sensor::update() 处理------这和你之前学过的 HallSensorEncoder 设计思路是一样的,也是 SimpleFOC 整个传感器层最重要的统一约定之一:

  • 子类负责提供当前的圈内机械角度
  • 基类负责把它整理成连续角度、整圈计数和速度

所以从架构上看,MagneticSensorSPI 虽然底层协议最复杂,但它在 Sensor 体系里的职责边界反而非常清晰。

七、完整调用链


如果把这一整套流程串起来,调用链其实很清楚:

用户代码调用 sensor.update()

Sensor::update()

→ 子类 getSensorAngle()

getRawCount()

read(angle_register)

→ 通过两帧 SPI 事务读出原始角度

→ 换算出圈内机械角度

→ 基类继续完成跨圈检测、连续角度维护和速度计算

这也是为什么前面第 5 篇先讲 Sensor 基类、第 7 篇和第 8 篇再讲具体传感器,是一个很合理的阅读顺序。你会发现:底层取数方式可以完全不同,但一旦进入 Sensor 层,对上层控制器暴露出来的接口却是统一的。

八、总结


MagneticSensorSPI 的架构可以用一句话概括:用两次 SPI 事务按协议读出单圈绝对角度,再交给基类 Sensor 做多圈累积和速度计算

HallSensor 的核心区别在这里:

HallSensor MagneticSensorSPI
角度来源 3路中断,状态机查表 SPI 轮询,读取角度寄存器
分辨率 6 步/电周期 16384 步/圈(绝对)
是否需要 update() 快速调用 是(否则漏中断) 是(但不漏事件,只影响速度精度)
多圈跟踪 electric_rotations 自己管 完全交给 Sensor 基类

如果说 HallSensor 解决的是"怎样把 3 路数字信号还原成电角度扇区 ",那么 MagneticSensorSPI 解决的就是"怎样按芯片协议稳定读出高分辨率机械角度"。

两篇连着看,会更容易看懂 SimpleFOC 传感器层的设计边界:

  • HallSensor,难点在状态机、方向判断和电角度回绕
  • MagneticSensorSPI,难点在 SPI 协议、帧格式和有效位提取
  • 但它们最终都会收敛到同一个 Sensor 抽象接口

顺手补一句:像 parity 这类协议字段,并不是所有配置都会启用。对 AMS 这类默认配置,它用于补齐命令帧;但对 MA730_SPI 这种配置,源码里就明确把 command_parity_bit 设成了 0

相关推荐
12.=0.2 小时前
【stm32_7】定时器的原理与应用、基本定时器、通用定时器、PWM、模拟脉冲信号的宽度、利用PWM控制外设、逻辑分析仪的使用
c语言·stm32·单片机·嵌入式硬件
Deitymoon2 小时前
STM32——振动传感器控制继电器
stm32·单片机·嵌入式硬件
Freak嵌入式2 小时前
亲测可用!可本地部署的 MicroPython 开源仿真器
ide·驱动开发·嵌入式·仿真·micropython·upypi
国产芯片设计2 小时前
DIY实战|0.8寸WiFi自动授时电子钟,国产数码管驱动芯片方案分享
stm32·单片机·mcu·51单片机·硬件工程
LCMICRO-133108477463 小时前
长芯微LD73360完全P2P替代AD73360,是一款工业电能计量6通道模拟输入前端(AFE) 处理器
stm32·单片机·嵌入式硬件·fpga开发·硬件工程·模拟前端afe
rit843249914 小时前
STM32 + DS3231 + TM1640 实时时钟数码管显示系统
stm32·单片机·嵌入式硬件
小懒懒️15 小时前
嵌入式常见通信协议学习——UART
stm32·uart·通信协议
zjxtxdy15 小时前
STM32开发
stm32·单片机·fpga开发
BT-BOX15 小时前
STM32简易数字电流表仿真_LCD1602显示
stm32·电流测量·lcd1602显示·电流表