上一篇帖子中介绍了本项目的整体方案和硬件设计,其中磁编采用了mps 的 MA600A,这是一颗功能丰富分辨率高的磁编芯片.
这两天调通了这颗芯片. 这个过程不算复杂,但中间有几个点挺值得记录:
一是开发前先用 MPS 提供的 Magnetic Simulation Tool 对旁轴磁铁结构做了仿真,提前看到了角度误差趋势;
二是 MA600A 的 SPI 协议本身比较直接,适合封成一个独立的小库;
三是实际硬件采用旁轴磁铁布局,后面的 lkscope 波形也和仿真预判基本对上了;
四是没有接串口的情况下,用 J-Link SWD + RTT 配合凌欧的 lkscope 实时看内部变量,调试效率还不错。
本文记录的是从磁铁部署仿真、官方手册读协议,到实现接口库,再到工程部署和上板测试的完整过程。标定部分还没做,等电机开环拖动调好之后再单独写。
机械设计图,MA600A和电机磁铁是旁轴布局
1. 硬件和工程环境
这次使用的硬件和软件环境如下:
MCU:AT32M412
磁编码器:MPS MA600A
通信接口:SPI2
调试器:J-Link,通过 SWD 连接
实时观测:凌欧 lkscope,通过 RTT 读取内部变量并绘制波形
固件构建:WSL 下 CMake/Ninja 构建
烧录调试:Windows 下 Keil + J-Link
MA600A 在工程中主要用于读取电机转子位置。硬件已经确定使用 SPI2,片选脚也由底层配置工具生成。由于后面还要做旁轴布局的标定,我在驱动设计时没有把代码直接绑死在 AT32 的 SPI 外设上,而是把芯片协议层和 MCU 适配层拆开。
2. 开发前先用 MPS 磁场仿真工具预判误差
在写驱动之前,我先用了 MPS 官方提供的 Magnetic Simulation Tool。这个网页工具可以按传感器系列、安装拓扑、磁铁类型、尺寸参数和传感器位置来估算磁场分量、BCT 建议值、非线性误差和角度曲线。
这个步骤对旁轴结构尤其有用。旁轴安装本来就不是最理想的结构,如果一开始不估算磁场和角度误差,后面上板看到角度不线性时,很难判断是软件通信问题、焊接问题,还是机械布局本身带来的结果。
第一步选择传感器系列。MA600A 属于 MagAlpha 系列,所以这里选 MagAlpha sensor。
第二步选择传感器和磁铁的相对位置。工具里提供了 End-Of-Shaft、Side-Shaft 和 Side-Shaft Orthogonal。我当前硬件属于旁轴结构,所以选择 Side-Shaft。这个页面也直接提示了旁轴方案属于 advanced solution,需要 BCT compensation。
第三步选择磁铁类型。工具支持圆柱磁铁、带孔圆柱磁铁、径向环形磁铁、轴向环形磁铁和两个半圆柱磁铁。我这里按实际使用的磁铁形态选择了带孔圆柱磁铁。
第四步填写结构参数。这里包括磁铁剩磁、磁铁高度、外径、内径、MA600A 型号、滤波截止频率,以及磁铁中心到传感器敏感点的距离。工具还提供了 advanced parameters,可以继续加入传感器偏移、传感器旋转、磁铁偏移、磁铁旋转和磁化方向误差。
这一步我按当前 PCB 和磁铁布局填入实际尺寸,用来模拟现有旁轴安装状态。
仿真结果页面会给出磁场分量、磁场比例 k、建议 BCT、修调轴方向、分辨率估算和角度非线性。更有用的是下面几张曲线:磁场分量随机械角度变化、补偿前后的角度输出曲线、角度误差曲线,以及 BCT、ETX/ETY、E、Resolution 这些参数的解释。
最后还可以生成 report。报告里会把输入参数和仿真结果整理在一起,方便后面复现实验条件。当前这组仿真里,报告记录的结构是 Side-Shaft,磁铁类型是 Cylinder with Hole,传感器型号是 MA600,并给出了 BCT、ETX/ETY、非线性误差和角度曲线。
这个仿真的意义不是替代实测,而是先给出一个预期:旁轴结构会带来明显的角度非线性,需要 BCT 和后续 correction table 来处理。后面我在板子上用 lkscope 看到的角度波形,确实能和开发前的仿真判断对应上。这样至少可以排除“是不是 SPI 读错了”这类方向上的误判。
3. 从手册确认 SPI 读数方式
MA600A 的 SPI 读角度比较简单。普通角度读取是 16-bit frame,主机发送 0x0000,从机返回 16-bit angle。
角度值是 0 到 65535 对应一圈:
angle_deg = raw_angle * 360.0f / 65536.0f;
手册里另一个比较有用的接口是 32-bit frame。主机在一次 /CS 拉低期间连续发送 32 个 clock,MA600A 返回:
前 16 bit:angle
后 16 bit:multi-turn 或 speed
第二个 16-bit 字段由 PRT(0x1C) 寄存器里的 MTSP 位决定:
MTSP = 0:返回 multi-turn
MTSP = 1:返回 speed
速度数据是一个 int16_t 二补码有符号数。手册给出的默认比例是:
5.722 rpm/LSB
也就是:
speed_rpm = speed_raw * 5.722f;
这个比例基于内部 100kHz 时钟。如果需要更高精度,可以打开 IF(0x0E).CK100,把相关时钟输出到 CK100 引脚,再按实际频率修正比例:
speed_rpm = speed_raw * 5.722f * f_ck100_khz / 100.0f;
4. 驱动库设计
我希望这个库以后能比较容易移植,所以把 MA600A 的协议层写成纯 C,不包含任何 AT32、SPI2、GPIO 之类的芯片符号。底层通信通过回调函数注入:
typedef struct ma600a_bus
{
void *user;
ma600a_status_t (*spi_transfer16)(void *user, uint16_t tx, uint16_t *rx);
ma600a_status_t (*spi_transfer8)(void *user, uint8_t tx, uint8_t *rx);
void (*cs_write)(void *user, uint8_t level);
void (*delay_us)(void *user, uint32_t us);
void (*delay_ms)(void *user, uint32_t ms);
} ma600a_bus_t;
这样 ma600a.c 只负责:
1. 按手册拼 SPI 命令
2. 控制 /CS 时序
3. 读写寄存器
4. 做基本数据转换
5. 返回统一状态码
而具体 SPI2 怎么收发、CS 脚怎么拉高拉低,放在 ma600a_at32_spi2.c 里。
当前已经实现的接口包括:
ma600a_status_t ma600a_init(ma600a_t *dev, const ma600a_bus_t *bus);
ma600a_status_t ma600a_read_angle_raw(ma600a_t *dev, uint16_t *angle);
ma600a_status_t ma600a_read_angle_deg(ma600a_t *dev, float *deg);
ma600a_status_t ma600a_set_mtsp_speed(ma600a_t *dev, uint8_t enable);
ma600a_status_t ma600a_read_angle_and_speed_raw(ma600a_t *dev, uint16_t *angle, int16_t *speed);
ma600a_status_t ma600a_read_speed_raw(ma600a_t *dev, int16_t *speed);
ma600a_status_t ma600a_write_bct(ma600a_t *dev, uint8_t bct, ma600a_bct_axis_t axis);
ma600a_status_t ma600a_read_bct(ma600a_t *dev, uint8_t *bct, ma600a_bct_axis_t *axis);
ma600a_status_t ma600a_write_zero(ma600a_t *dev, uint16_t zero);
ma600a_status_t ma600a_read_zero(ma600a_t *dev, uint16_t *zero);
ma600a_status_t ma600a_set_current_angle_as_zero(ma600a_t *dev, uint16_t *zero);
ma600a_status_t ma600a_write_correction_table(ma600a_t *dev, const int8_t corr[32]);
ma600a_status_t ma600a_read_correction_table(ma600a_t *dev, int8_t corr[32]);
ma600a_status_t ma600a_clear_correction_table(ma600a_t *dev);
ma600a_status_t ma600a_store_config_to_nvm(ma600a_t *dev);
ma600a_status_t ma600a_store_correction_to_nvm(ma600a_t *dev);
ma600a_status_t ma600a_restore_from_nvm(ma600a_t *dev);
float ma600a_speed_raw_to_rpm(int16_t speed);
float ma600a_speed_raw_to_rpm_with_ck100(int16_t speed, float f_ck100_khz);
速度读取这里有一个小细节。32-bit frame 要求 /CS 在两个 16-bit 传输期间保持低电平,所以不能简单调用两次“读角度”的 16-bit 封装,因为那个封装每次都会独立拉低和拉高 CS。实现里单独做了一个 32-bit zero frame helper:
static ma600a_status_t ma600a_transfer32_zero(ma600a_t *dev, uint16_t *angle, uint16_t *data)
{
ma600a_status_t status;
dev->bus.cs_write(dev->bus.user, MA600A_CS_LOW);
status = dev->bus.spi_transfer16(dev->bus.user, 0x0000u, angle);
if(status == MA600A_OK)
{
status = dev->bus.spi_transfer16(dev->bus.user, 0x0000u, data);
}
dev->bus.cs_write(dev->bus.user, MA600A_CS_HIGH);
if(status != MA600A_OK)
{
return MA600A_ERR_BUS;
}
return MA600A_OK;
}
设置 MTSP 时也没有直接覆盖整个 PRT 寄存器,而是读改写,只改 bit7。这样不会破坏 parity check 和 test angle 相关配置。
5. AT32 SPI2 适配
AT32 侧的适配文件只做底层工作:
1. SPI2 发送/接收 8 bit
2. 拼出 16 bit 传输
3. 控制 SPI2_CS_GPIO_PORT / SPI2_CS_PIN
4. 提供 us/ms 延时
5. 返回 ma600a_bus_t 给协议层使用
工程中的 ma600a_at32_spi2_bus_get() 会返回一个静态 bus:
const ma600a_bus_t *ma600a_at32_spi2_bus_get(void)
{
static const ma600a_bus_t bus = {
0,
ma600a_at32_spi2_transfer16,
ma600a_at32_spi2_transfer8,
ma600a_at32_spi2_cs_write,
ma600a_at32_spi2_delay_us,
ma600a_at32_spi2_delay_ms,
};
return &bus;
}
这层代码依赖 AT32 的外设库和 WorkBench 生成的引脚宏,但 MA600A 协议层不感知这些细节。
6. 工程集成
CMake 和 Keil 工程都加入了 MA600A 相关源码。main.c 中,在 SPI2 初始化完成后初始化调试模块:
wk_spi2_init();
ma600a_debug_init();
主循环中周期轮询:
while(1)
{
rt_thread_mdelay(10);
ma600a_debug_poll();
gpio_bits_toggle(LED_GPIO_PORT, LED_PIN);
}
调试模块里定义了一组全局变量,方便 Keil Watch 或 RTT 工具观察:
volatile uint16_t g_ma600a_raw_angle;
volatile float g_ma600a_angle_deg;
volatile int16_t g_ma600a_speed_raw;
volatile float g_ma600a_speed_rpm;
volatile int32_t g_ma600a_status;
volatile uint32_t g_ma600a_sample_count;
volatile uint32_t g_ma600a_error_count;
volatile uint8_t g_ma600a_bct;
volatile uint8_t g_ma600a_axis;
ma600a_debug_init() 会做三件事:
1. 绑定 SPI2 bus
2. 读取一次 BCT 配置
3. 设置 PRT.MTSP = 1,让 32-bit frame 第二段返回 speed
之后 ma600a_debug_poll() 每次通过 32-bit frame 同时读取角度和速度。
7. 编译和烧录
固件构建在 WSL 下完成。当前构建通过后资源占用如下:
cd /mnt/e/WorkSpaces/2_电机驱动/MPS_MotorDriver && cmake --build --preset Debug
[1/5] Building C object CMakeFiles/MPS_MotorDriver.dir/project/src/ma600a_debug.c.obj
[2/5] Building C object CMakeFiles/MPS_MotorDriver.dir/middlewares/msp/ma600/ma600a.c.obj
[3/5] Building C object CMakeFiles/MPS_MotorDriver.dir/middlewares/msp/ma600/ma600a_at32_spi2.c.obj
[4/5] Building C object CMakeFiles/MPS_MotorDriver.dir/project/src/main.c.obj
[5/5] Linking C executable MPS_MotorDriver.elf
Memory region Used Size Region Size %age Used
FLASH: 26500 B 128 KB 20.22%
RAM: 3520 B 16 KB 21.48%
[5/5] Linking C executable MPS_MotorDriver.elf
Memory region Used Size Region Size %age Used
FLASH: 26500 B 128 KB 20.22%
RAM: 3520 B 16 KB 21.48%
Keil 工程也同步加入了这些文件,最终编译结果:
compiling main.c...
linking...
Program Size: Code=15198 RO-data=866 RW-data=200 ZI-data=3384
FromELF: creating hex file...
".\objects\MPS_MotorDriver.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed: 00:00:01
烧录使用 Keil 调用 J-Link,成功下载
8. 用 lkscope 通过 RTT 看实时波形
这块是实际调试时最顺手的部分。因为当前阶段没有接串口,如果只靠 Keil Watch,变量能看,但看连续变化不方便。后来改成用 J-Link 的 SWD 连接,通过 RTT 把内部变量实时读出来,再用凌欧的 lkscope 显示成波形。
这种方式对编码器调试很适合。角度、速度、错误计数这些变量都可以在程序运行时连续观察,不需要频繁打断点,也不会因为暂停 CPU 影响波形判断。
我主要观察了这些变量:
g_ma600a_raw_angle
g_ma600a_angle_deg
g_ma600a_speed_raw
g_ma600a_speed_rpm
g_ma600a_status
g_ma600a_sample_count
g_ma600a_error_count
目前看到的现象:
1. 静止时,角度数据可以稳定读取
2. 手动转动磁铁时,角度值连续变化
3. 正反方向转动时,速度值能体现方向
4. g_ma600a_sample_count 持续递增
5. g_ma600a_error_count 没有持续增加
如果只是验证通信接口,做到这一步基本就能确认:SPI 时序、CS 控制、角度读取、速度读取、寄存器写入这些关键路径是通的。
9. 旁轴磁铁对角度精度的影响
这块是后续必须处理的问题。
从 lkscope 的角度波形看,旁轴磁铁布局对角度读取精度影响很明显。理想同轴安装时,角度输出应该和机械角度接近线性;现在由于磁铁不是在传感器中心正上方旋转,磁场矢量变化会带来周期性误差。转动过程中能看到角度变化不是完全均匀的。
这个现象和开发前用 MPS 仿真工具得到的预判是一致的。仿真阶段已经能看到旁轴结构下角度曲线存在非线性,只是实际误差大小还要受磁铁装配、传感器位置、气隙、磁铁磁化一致性影响。实际波形能复现类似趋势,说明目前遇到的精度问题主要来自结构和磁场条件,而不是 SPI 通信接口本身。
MA600A 提供了几类和安装误差相关的配置能力:
BCT:用于补偿磁场幅值不平衡
ZERO:用于设置零位
Correction Table:用于做一圈内的角度误差修正
NVM Store:用于把配置保存到芯片内部非易失存储
当前代码已经把这些基础接口先实现出来,后续可以在此基础上做自动标定流程。不过标定实现不放在这篇里展开。
10. 后续标定思路
标定这部分先不在本帖实现,只记录一下后续方向。我目前考虑用匀速拖动电机的方式做标定。基本思路是让电机以尽可能稳定的速度转动,连续采集 MA600A 输出角度。因为理想情况下匀速转动对应一条线性的角度曲线,所以可以从实际输出中提取周期性误差,再生成 correction table。
这个方法是可行的,但对测试条件有要求:
1. 拖动速度要足够稳定
2. 采样周期抖动要小
3. 最好采多圈并做平均
4. 标定时速度不要太高,避免滤波延迟和速度纹波放大误差
5. 如果有外部高精度编码器作为基准,标定结果会更可靠
没有外部基准编码器时,也可以做基于匀速假设的标定,但误差里会混入电机转速波动。这个要等电机开环拖动调顺之后再做。
11. 小结
这次 MA600A 调试从磁铁部署仿真和手册协议开始,先用 MPS Magnetic Simulation Tool 对当前旁轴结构做预判,再实现一个和 MCU 解耦的基础接口库,然后在 AT32M412 工程中接入 SPI2,最后通过 J-Link SWD + RTT + lkscope 做实时变量观测。
目前已经验证的功能包括:
1. SPI 读取 16-bit 角度
2. SPI 32-bit frame 同时读取角度和速度
3. 速度 raw 值转换 rpm
4. BCT 读写
5. 零位读写
6. correction table 读写
7. 配置保存到 NVM
8. Keil 工程编译和烧录
9. lkscope 实时波形观察
基础通信和数据读取已经跑通。真正影响最终角度精度的地方,已经从软件接口转移到了机械布局和标定质量上。旁轴磁铁能用,但不能指望不标定就得到很好的线性度。下一步先把电机开环拖动调稳定,再单独做 correction table 标定。
















