导言
github 源码:
- https://github.com/simplefoc/Arduino-FOC/blob/v2.3.2/src/sensors/MagneticSensorSPI.h
- https://github.com/simplefoc/Arduino-FOC/blob/v2.3.2/src/sensors/MagneticSensorSPI.cpp

在上一篇HallSensor里,你看到的是一种低分辨率、离散扇区式的位置反馈:一个电周期只有 6 个状态,重点在状态机和扇区切换。
但如果换成 SPI 磁编码器,问题就完全变了:
- 为什么同样是"读位置",这里不再是查表,而是读一颗专用芯片的寄存器?
- 为什么看起来只是读一个角度值,源码里却要处理 奇偶校验、两帧传输、位对齐、掩码?
- 它最终又是怎么接回
Sensor基类,继续复用多圈角度和速度计算这套通用逻辑的?
这篇就来拆 MagneticSensorSPI.cpp/.h。如果说 HallSensor 代表的是"分辨率低,但结构简单 ",那么 MagneticSensorSPI 代表的就是"分辨率高,但通信协议更复杂"。
先说结论:MagneticSensorSPI 的核心并不是"读一个 SPI 寄存器"这么简单,而是按不同芯片的 SPI 协议,正确拿到单圈机械角度,再交给 Sensor 基类做连续角度和速度管理。
我们按以下五步来拆解:

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

关键点总结:
- 这类磁编码器通过芯片内部的磁场感应与角度解算电路,把转子当前位置转换成寄存器中的绝对角度值。对
MagneticSensorSPI来说,源码真正关心的不是芯片内部模拟细节,而是:最终能不能按协议把这个角度寄存器读出来。 - 对这类 AMS 传感器来说,SPI 一次传输通常是 16-bit 帧 。但要注意:发送给芯片的命令帧 和芯片返回的数据帧 ,高位字段含义并不完全一样。命令帧里会带上
R/W与奇偶校验;返回帧里高位通常是校验/错误标志,低 14 位才是角度数据。这正是后面read()函数要做位移和掩码的根本原因。 - 在 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_SPI 的 command_rw_bit=0、command_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 件事:
- 保存 SPI 外设对象指针
- 根据时钟、位序、模式生成
SPISettings - 初始化片选脚,并先拉高
CS - 调用
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
}

先按执行顺序,把它翻译成人话:
- 先根据芯片协议,拼出一帧
command - 如果该芯片需要
R/W位,就把"读寄存器"标志写进去 - 如果该芯片需要奇偶校验位,就把 parity 也补进去
- 拉低
CS,发送第一帧命令 - 拉高
CS,等待器件准备返回数据 - 再次拉低
CS,发送空数据,同时把上一帧请求对应的返回值读出来 - 对返回值做右移和掩码,只保留角度有效位
这里有两个容易误解的点。
第一个点:这不是"连续发 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 = 14cpr = 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_resolution 的 MagneticSensorSPI 对象,后续实例会复用第一次生成的掩码,理论上就可能出现错误读数。
如果你后面自己写类似驱动,更稳妥的写法通常是把它改成普通局部变量,或者放到对象成员里按实例管理。
六、角度换算------从原始计数到弧度
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() 处理------这和你之前学过的 HallSensor、Encoder 设计思路是一样的,也是 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。