MagneticSensorSPI is the high-resolution position sensor driver in SimpleFOC for SPI magnetic encoders. Its core value is to reliably read the single-turn mechanical angle according to each chip protocol, then reuse the Sensor base class for multi-turn angle tracking and velocity management. This design hides the complexity of register reads, parity checks, two-frame transactions, and valid-bit extraction. Keywords: SimpleFOC, SPI magnetic encoder, FOC motor control
The technical specification snapshot provides the context
| Parameter | Description |
|---|---|
| Project | Arduino-FOC / SimpleFOC |
| Version | v2.3.2 |
| Language | C++ |
| Communication Protocol | SPI |
| Typical Chips | AS5147 / AS5047 / AS5048 / MA730 |
| Angle Resolution | Commonly 14-bit |
| Typical Clock | 1 MHz |
| Core Dependencies | SPIClass, SPISettings, Sensor base class |
| Repository | github.com/simplefoc/Arduino-FOC |
| Star Count | Not provided in the source; refer to live GitHub data |
This article defines the responsibility boundary of MagneticSensorSPI
MagneticSensorSPI does not implement the full closed-loop control stack. It does exactly one thing: read the absolute angle output from a magnetic encoder correctly over SPI. After that, continuous angle tracking, multi-turn accumulation, and velocity calculation remain the responsibility of the Sensor base class.
That means it acts as a protocol adaptation layer. For upper-layer controllers, the interface stays uniform. For lower-layer hardware, device-specific differences are absorbed by configuration.
AI Visual Insight: This figure introduces the core theme of the article. It highlights that once feedback moves from HallSensor sector-based states to SPI absolute angle feedback, the source-code focus shifts from state machines to register protocols, frame formats, and valid-bit extraction.
SPI magnetic encoders and Hall sensors differ in fundamental ways
Hall-based solutions usually expose only a limited number of states, which makes them suitable for low-resolution position feedback. SPI magnetic encoders provide high-resolution absolute angle values directly, so the main challenge is not lookup logic, but stable register reads and data cleanup.
// The subclass only provides the mechanical angle within one revolution
float MagneticSensorSPI::getSensorAngle() {
return (getRawCount() / (float)cpr) * _2PI; // Convert the raw count to radians
}
This code shows that the class fundamentally outputs a single-turn angle, not a continuous angle.
Chip protocol differences are parameterized through a configuration struct
SimpleFOC does not hardcode MagneticSensorSPI for a single chip. Instead, it defines MagneticSensorSPIConfig_s and abstracts SPI mode, resolution, angle register address, data start bit, read/write bit, and parity bit into parameters.
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;
};
This struct defines the minimum required parameter set for the protocol adaptation layer.
AI Visual Insight: This image corresponds to the configuration-layer analysis. It shows that although different magnetic encoders all return angle data over SPI, their SPI modes, register addresses, and valid-bit layouts differ. As a result, the driver must abstract protocol parameters through a struct instead of hardcoding a single chip model.
The default configuration reflects the compatibility strategy for AMS chips and MA730
AS5147, AS5047, and AS5048 use closely related protocols and can share a configuration. MA730 does not require the same read/write bit or parity bit, so those fields are set to 0 in its configuration.
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
};
This default configuration defines the typical SPI frame semantics for AMS 14-bit encoders.
The two constructors serve fast integration and protocol extension separately
The first constructor is suitable for scenarios that match the default AMS-compatible behavior. You only pass the chip-select pin, resolution, and register address. The second constructor accepts a full configuration struct directly, which fits chips with more obvious protocol differences.
MagneticSensorSPI::MagneticSensorSPI(int cs, int _bit_resolution, int _angle_register){
chip_select_pin = cs;
angle_register = _angle_register ? _angle_register : DEF_ANGLE_REGISTER;
cpr = _powtwo(_bit_resolution); // Compute counts per revolution using integer shifting
spi_mode = SPI_MODE1;
clock_speed = 1000000;
bit_resolution = _bit_resolution;
command_parity_bit = 15;
command_rw_bit = 14;
data_start_bit = 13;
}
This code shows that the default construction path directly embeds AMS-style protocol assumptions.
AI Visual Insight: This image focuses on the constructor and parameter initialization flow. It highlights that cpr, register address, SPI mode, and control-bit positions are fixed when the object is created, so the later read() call can rely on these members to assemble commands and parse data.
cpr is the reference value for all later angle conversion
cpr = 1 << bit_resolution defines how many discrete counts exist in one mechanical revolution. For a 14-bit encoder, cpr = 16384. Every later conversion from raw counts to radians depends on this value.
init() completes peripheral binding and base-class initialization
The constructor only prepares the object in memory. The method that actually makes the sensor usable is init(). It binds the SPI object, configures transaction parameters, initializes the chip-select pin, and invokes Sensor::init() for the base-class state.
void MagneticSensorSPI::init(SPIClass* _spi){
spi = _spi; // Store the SPI peripheral object
settings = SPISettings(clock_speed, MSBFIRST, spi_mode); // Configure timing parameters
pinMode(chip_select_pin, OUTPUT);
spi->begin();
digitalWrite(chip_select_pin, HIGH); // Release chip select by default
this->Sensor::init(); // Initialize base-class state
}
This code connects protocol-layer initialization with sensor-abstraction-layer initialization.
read() is the true core of the entire driver
The genuinely complex part is not “calling SPI once,” but respecting the sensor protocol and completing a two-frame transaction. The first frame sends the read command, and the second frame fetches the data corresponding to the previous frame. This timing pattern is typical for many absolute encoders.
word MagneticSensorSPI::read(word angle_register){
word command = angle_register;
if (command_rw_bit > 0) {
command = angle_register | (1 << command_rw_bit); // Set the read-command bit
}
if (command_parity_bit > 0) {
command |= ((word)spiCalcEvenParity(command) << command_parity_bit); // Add even parity
}
spi->beginTransaction(settings);
digitalWrite(chip_select_pin, LOW);
spi->transfer16(command); // First frame: send the request
digitalWrite(chip_select_pin, HIGH);
delayMicroseconds(1);
digitalWrite(chip_select_pin, LOW);
word register_value = spi->transfer16(0x00); // Second frame: read the returned value
digitalWrite(chip_select_pin, HIGH);
spi->endTransaction();
register_value = register_value >> (1 + data_start_bit - bit_resolution); // Right-shift to align the data bits
word data_mask = 0xFFFF >> (16 - bit_resolution); // Generate the valid-bit mask
return register_value & data_mask; // Clear protocol-level high bits
}
This code fully captures four critical actions: command construction, two-frame communication, bit alignment, and mask extraction.
AI Visual Insight: This figure maps to the read() execution path. It shows chip-select toggling, first-frame command transmission, second-frame data retrieval, and final bit processing. It emphasizes that reading an angle from this type of encoder is essentially a register access with pipeline latency, not an immediate single-frame return.
Shift and mask logic determines whether the reading is correct
Take AS5147 as an example: its 14-bit angle data sits in the lower 14 bits of the returned frame, so the shift amount may be 0. For other devices, however, the valid-bit start position differs, so the driver must right-shift first and then apply a mask.
A potential risk also lives here: if the mask were implemented as a function-level static, instances with different resolutions could incorrectly reuse the same cached value. A safer implementation uses a normal local variable or an object member.
The angle-conversion stage keeps the responsibility model clean
The driver layer first obtains the raw count, then performs a standard conversion to radians. For a 14-bit encoder, the midpoint value 8192 maps to approximately π, which represents half a turn.
int MagneticSensorSPI::getRawCount() {
return (int)MagneticSensorSPI::read(angle_register); // Read the raw count
}
float MagneticSensorSPI::getSensorAngle() {
return (getRawCount() / (float)cpr) * _2PI; // Map to [0, 2π)
}
This code converts register counts into a standard mechanical angle that the controller can consume.
AI Visual Insight: This image emphasizes the mapping from raw counts to mechanical angle. It typically illustrates single-turn absolute position using a circle or quantized intervals, which helps explain the one-to-one relationship among cpr, radian conversion, and single-turn angle output.
The full call chain demonstrates the value of the Sensor abstraction
After the upper layer calls sensor.update(), the base class calls back into the subclass through getSensorAngle(). In other words, MagneticSensorSPI only needs to provide a reliable angle within one revolution, while wrap detection, multi-turn tracking, and velocity estimation are still handled uniformly by Sensor.
AI Visual Insight: This figure shows the call chain from the user invocation to base-class update, then to subclass angle reading, mechanical-angle return, and finally multi-turn and velocity processing. It reflects the SimpleFOC sensor-layer architecture: diverse hardware at the bottom, unified abstraction at the top.
This implementation reflects the typical SimpleFOC design philosophy
Both are position sensors, but their difficulties differ. For HallSensor, the challenge lies in state machines and sector transitions. For MagneticSensorSPI, the challenge lies in SPI protocol handling and frame parsing. Even so, both ultimately implement the same Sensor interface, which preserves controller-layer stability and replaceability.
FAQ answers the most practical implementation questions
Q1: Why does read() perform two transfer16() calls? Isn’t one enough?
A: Many SPI magnetic encoders use a pipelined mechanism: the first frame sends the request, and the second frame retrieves the result. The first frame writes the register read command, while the second frame returns the data for the previous request. The two-frame sequence is a protocol requirement, not redundant logic.
Q2: Why are command_rw_bit and command_parity_bit sometimes 0?
A: That means the target chip does not require those control bits. SimpleFOC uses a configuration struct to hide protocol differences, so the same high-level logic can support both AMS devices and simpler frame formats such as MA730.
Q3: Why doesn’t MagneticSensorSPI output velocity directly?
A: Because SimpleFOC splits responsibilities deliberately. The subclass only samples the current single-turn angle, while velocity, multi-turn angle, and wrap handling are managed uniformly by the Sensor base class. That design improves reuse and maintainability.
The core takeaway clarifies the architectural role of MagneticSensorSPI
This article reconstructs and explains MagneticSensorSPI.cpp/.h in SimpleFOC v2.3.2, focusing on the configuration struct, constructors, init() initialization, two-frame SPI register reads, parity handling, and bit alignment. It also shows how the driver reuses the Sensor base class to provide continuous angle and velocity computation.