【2026 机器人模块大赛】微型机器人单关节闭环驱动模块: MA600A 从手册到调通

上一篇帖子中介绍了本项目的整体方案和硬件设计,其中磁编采用了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-ShaftSide-ShaftSide-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 标定。