一、所选任务介绍
本次项目选自电子森林(eetree)举办的ADMT4000多圈磁传感器开发活动。活动旨在让开发者深入了解ADMT4000这款高性能多圈绝对角度传感器的特性,并掌握其在嵌入式系统中的应用方法。
任务要求:
1. 手动旋转磁铁,通过串口实时显示ADMT4000的圈数和角度
2. 验证断电后数据是否保持
3. 通过SPI读取ADMT4000的多圈圈数和精确角度
4. 串口实时打印当前圈数、角度值
5. 手动旋转到任意位置后断电,重新上电,串口显示的圈数和角度与断电前一致
6. 在46圈范围内多次测试,记录并展示断电前后的数据对比
二、项目描述
2.1 项目概述
本项目基于STM32F103微控制器和ADMT4000多圈绝对角度传感器,构建了一个完整的磁传感器数据采集和断电保持验证系统。验证了完整的功能,并实现了步进电机圈数角度掉电不丢失的功能。
2.2 硬件平台

- STlink(带有串口转usb功能)
- STM32F103ZET6开发板
- ADMT4000评估版
- 42步进电机(轴头带径向磁铁)
- A4988模块+扩展板
- 数控电源
2.3 注意事项
1. 注意电机应选用转子轴头带有径向磁铁的款式

- 模块设计尺寸为42步进电机大小,需控制好芯片与磁铁之间的间隙

三、系统架构框图
四、关键代码解析
/**
* @brief 主函数
*/
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI1_Init();
MX_USART1_UART_Init();
/* 初始化平台层 (DWT计数器等) */
admt_platform_init();
printf("\nADMT4000 test porgrum (STM32)\n");
/* 初始化 ADMT4000 (带故障检查) */
admt_err_t ret = admt4000_init_with_fault_check();
if (ret != ADMT_OK) {
printf("ADMT4000 fail: %d\n", ret);
// while (1);
}
printf("开始实时监控角度传感器和圈数...\n");
printf("按 BOOT0 键执行线圈复位\n");
/* 短暂延时让传感器稳定 */
admt_platform_delay_ms(100);
uint16_t turns_raw, angle_05_raw;
/* 主循环 */
while (1)
{
if((HAL_GetTick() % 1000) == 0)
{
/* 读取圈数和角度寄存器 */
ret = admt4000_read_registers_dual(0x03, 0x05, &turns_raw, &angle_05_raw);
if (ret == ADMT_OK)
{
float turns = admt4000_convert_to_turns_with_fraction(turns_raw);
float magnetic_angle = admt4000_convert_to_angle(angle_05_raw);
float single_turn_angle = admt4000_extract_single_turn_angle(turns_raw);
/* 打印圈数 */
if (turns == -999.0f) {
printf("圈数无效, ");
} else if (turns == -998.0f) {
printf("圈数超出范围, ");
} else {
printf("%.3f圈, ", turns);
}
/* 打印角度 */
printf("磁角度(0x05): %.2f°, 单圈角度(0x03[9:0]): %.2f°\n",
magnetic_angle, single_turn_angle);
}
else
printf("读取失败 (CRC错误或其他)\n");
/* 扫描按键 */
button_scan();
}
/* 电机旋转 */
motor_rotate_ctrl(1, 3000);
}
}
说明:
- 初始化各模块
- 控制电机旋转,并每秒检测一次圈数和角度
/**
* @brief 带故障检查的初始化
*/
admt_err_t admt4000_init_with_fault_check(void)
{
admt_err_t ret;
/* 1. 基本初始化 */
// MX_SPI1_Init();
g_initialized = true;
/* 2. 读取FAULT寄存器, 验证是否为0xFFFF */
uint16_t fault_reg;
ret = admt4000_read_fault_raw(&fault_reg);
if (ret != ADMT_OK) {
ADMT_LOG_E("读取FAULT寄存器失败");
return ret;
}
ADMT_LOG_I("FAULT寄存器初始值: 0x%04X", fault_reg);
if (fault_reg == 0xFFFF) {
ADMT_LOG_I("✓ 寄存器功能正常");
} else {
ADMT_LOG_E("期望0xFFFF, 读到0x%04X", fault_reg);
}
/* 3. 向FAULT寄存器写入0x0000, 清除所有锁存的故障标志 */
ADMT_LOG_I("尝试清除FAULT寄存器...");
ret = admt4000_clear_fault();
if (ret != ADMT_OK) {
ADMT_LOG_E("清除FAULT寄存器失败 (ret=%d)", ret);
/* 注意: 某些版本的ADMT4000 FAULT寄存器是只读的, 清除操作可能不会返回0x0000 */
/* 继续执行, 不在此处返回错误 */
} else {
ADMT_LOG_I("✓ 清除FAULT寄存器成功");
}
/* 4. 再次读取FAULT寄存器, 确认状态 */
ret = admt4000_read_fault_raw(&fault_reg);
if (ret != ADMT_OK) {
ADMT_LOG_E("再次读取FAULT寄存器失败");
return ret;
}
ADMT_LOG_I("FAULT寄存器清除后: 0x%04X", fault_reg);
/* 解析FAULT寄存器各位的含义 */
if (fault_reg == 0x0000) {
ADMT_LOG_I("✓ 故障标志已清除, 传感器就绪");
} else {
/* 注意: ADMT4000的FAULT寄存器可能是只读的, 某些位可能表示上电自检状态 */
/* 如果对应位(严重故障位)都为0, 则认为初始化成功 */
ADMT_LOG_W("FAULT寄存器为0x%04X", fault_reg);
if (fault_reg & ADMT_FAULT_AMR_RADIUS){
ADMT_LOG_W(" - AMR半径检测失败");
return ADMT_ERR; }
if (fault_reg & ADMT_FAULT_TURN_CROSSCHK){
ADMT_LOG_W(" - 圈数交叉检查失败");
return ADMT_ERR; }
if (fault_reg & ADMT_FAULT_GMR_STATE){
ADMT_LOG_W(" - GMR状态异常");
return ADMT_ERR; }
if (fault_reg & ADMT_FAULT_ECC_2BIT){
ADMT_LOG_W(" - ECC 2位错误");
return ADMT_ERR; }
if (fault_reg & ADMT_FAULT_NVM_CRC){
ADMT_LOG_W(" - NVM CRC错误");
return ADMT_ERR; }
if (fault_reg & ADMT_FAULT_VDRIVE_OV){
ADMT_LOG_W(" - VDRIVE过压");
return ADMT_ERR; }
if (fault_reg & ADMT_FAULT_VDRIVE_UV){
ADMT_LOG_W(" - VDRIVE欠压");
return ADMT_ERR; }
if (fault_reg & ADMT_FAULT_VDD_OV){
ADMT_LOG_W(" - VDD过压");
return ADMT_ERR; }
if (fault_reg & ADMT_FAULT_VDD_UV){
ADMT_LOG_W(" - VDD欠压");
return ADMT_ERR; }
}
return ADMT_OK;
}
说明:
- 用SPI与ADMT4000通信
- 读取FAULT寄存器,并写入FAULT寄存器,验证初始化是否成功
- 若初始化失败,检测失败原因
/**
* @brief SPI传输函数
* @param tx_data: 发送数据缓冲区
* @param rx_data: 接收数据缓冲区
* @param length: 数据长度(字节)
* @return admt_err_t: 错误码
*/
admt_err_t admt_platform_spi_transfer(const uint8_t *tx_data,
uint8_t *rx_data,
uint16_t length)
{
if (tx_data == NULL || rx_data == NULL || length == 0) {
return ADMT_ERR_INV_ARG;
}
/* 拉低CS(片选) */
HAL_GPIO_WritePin(ADMT_CS_PORT, ADMT_CS_PIN, GPIO_PIN_RESET);
/* SPI传输 */
HAL_StatusTypeDef status = HAL_SPI_TransmitReceive(&hspi1,
(uint8_t*)tx_data,
rx_data,
length,
100); // 100ms超时
/* 拉高CS */
HAL_GPIO_WritePin(ADMT_CS_PORT, ADMT_CS_PIN, GPIO_PIN_SET);
if (status != HAL_OK) {
return ADMT_ERR;
}
return ADMT_OK;
}
说明:
- 此函数封装了STM32 HAL库的SPI操作
- 严格控制CS信号的时序,确保在整个传输期间保持低电平
- 添加了参数验证和错误处理
/**
* @brief 执行线圈复位
*/
static void perform_coil_reset(void)
{
uint64_t current_time = admt_platform_get_time_us() / 1000; // 转换为毫秒
if (current_time - last_reset_time < COIL_RESET_COOLDOWN_MS)
{
uint64_t remaining_time = (COIL_RESET_COOLDOWN_MS - (current_time - last_reset_time)) / 1000;
printf("复位冷却中, 还需等待 %lu 秒\n", (unsigned long)remaining_time);
return;
}
if (reset_in_progress) {
printf("复位正在执行中...\n");
return;
}
printf("开始线圈复位...\n");
reset_in_progress = true;
last_reset_time = current_time;
/* 拉高复位引脚 */
HAL_GPIO_WritePin(COIL_RESET_PORT, COIL_RESET_PIN, GPIO_PIN_SET);
/* 延时 ms */
admt_platform_delay_ms(COIL_RESET_PULSE_MS);
/* 拉低复位引脚 */
HAL_GPIO_WritePin(COIL_RESET_PORT, COIL_RESET_PIN, GPIO_PIN_RESET);
reset_in_progress = false;
printf("复位脉冲结束\n");
// ADMT4000 Sensor Rest
HAL_GPIO_WritePin(SENSOR_RESET_PORT, SENSOR_RESET_PIN, GPIO_PIN_RESET);
HAL_Delay(20);
HAL_GPIO_WritePin(SENSOR_RESET_PORT, SENSOR_RESET_PIN, GPIO_PIN_SET);
// 等待传感器稳定(关键!)
HAL_Delay(200);
}
说明:
- 线圈复位函数
- 控制对应引脚,使电容放电产生复位磁场
- 线圈复位后使ADMT4000芯片进行复位
/**
* @brief 将绝对角度寄存器转换为带小数的圈数
* @param raw_value: 原始寄存器值(0x03寄存器)
* @return float: 圈数
* -999.0 表示无效 (turn count = 0b110110)
* -998.0 表示超出范围 (turn count > 46)
*
* @note 寄存器格式:
* [15:10] = 圈数 (6位, 有符号)
* [9:0] = 单圈角度 (10位, 0-1023)
*/
float admt4000_convert_to_turns_with_fraction(uint16_t raw_value)
{
/* 提取整圈数 [15:10] */
uint8_t turn_count = (raw_value >> 10) & 0x3F;
/* 提取单圈角度值 [9:0] */
uint16_t single_turn_angle = raw_value & 0x3FF;
/* 检查是否为invalid turn count (0b110110 = 54) */
if (turn_count == 0b110110) {
return -999.0f;
}
/* 检查是否为两补码负数 (>= 0b110111 = 55) */
if (turn_count >= 0b110111) {
int8_t signed_turn = (int8_t)(turn_count << 2) >> 2;
return (float)signed_turn + (float)single_turn_angle / 1024.0f;
}
/* 检查是否超出有效范围 (0-46有效) */
if (turn_count > 46) {
ADMT_LOG_W("圈数超出有效范围: %d (最大46)", turn_count);
return -998.0f; // 表示超出范围
}
/* 正常正数圈数 + 小数部分 */
return (float)turn_count + (float)single_turn_angle / 1024.0f;
}
说明:
- 从ABSANGLE寄存器(地址0x03)提取圈数和角度
- 圈数占用6位([15:10]),支持-10到+46圈
- 角度占用10位([9:0]),精度为1/1024圈
- 处理特殊值(无效值、超出范围)
- 支持有符号圈数(负数圈数)
五、实物演示
- 驱动电机转动检测相应的圈数

- 手动旋转电机检测相应的圈数

- 圈数重置

- 圈数掉电不丢失

六、遇到的难点及解决方法
6.1 移植驱动问题
将ESP32平台的驱动移植到STM32平台时,主要要解决以下问题:
- ESP-IDF特有的API(如`spi_device_transmit`)在STM32上不存在
- 错误码类型不同(`esp_err_t` vs 自定义错误码)
- 日志系统差异(`ESP_LOG` vs `printf`)
解决方案则是设计平台抽象层,创建`admt4000_port.h/.c`文件,定义通用的接口函数(如`admt_platform_spi_transfer`),在不同平台仅需将这个接口的底层实现即可。错误码类型不同则是将用到的错误码进行手动定义一遍。日志输出函数ESP_LOG重定向到printf函数。
6.2ADMT4000磁圈重置问题
通过ADI官方的应用笔记,了解到了重置的方式。该活动提供的ADMT4000评估板采用了瞬时提供强磁场的方式进行重置,活动的直播讲解了实现的原理,但并没细说重置的流程或者说时序。

复位电路如上图,其中将SHDN拉高则会开始向电容充电,将COIL_RS脚拉高则会释放电容存储的电能,电流流过线圈从而产生一个瞬时强磁场使ADMT4000磁圈重置。所以引脚的配置则是SHDN脚始终拉高(不需要重置功能时拉低即可),主控MCU主要控制COIL_RS脚,需要时将其拉高一段时间在拉低。
但其实经过上述操作后不能直接去读取ADMT4000的寄存器值,还需要将ADMT4000芯片重置一下,所以在评估板中,也引出了ADMT4000的RST复位引脚,则完整的重置流程是:提前将SHDN脚拉高使电容充电 → COIL_RS脚拉高一段时间在拉低 → ADMT4000的RST复位引脚拉低一段时间在拉高,此时则可以继续读取ADMT4000的寄存器值。
七、对本次活动的心得体会
通过本次ADMT4000多圈磁传感器开发活动,我收获颇丰,不仅深入掌握了SPI通信协议的时序控制、CRC5校验算法实现、多圈角度数据解析等核心技术,还完成了从ESP32到STM32平台的驱动移植工作,对嵌入式软件的跨平台架构设计、分层解耦思想有了更深刻的理解,也在调试SPI时序、解决数据解析边界问题的过程中极大提升了硬件问题定位和解决能力。本次活动选题非常贴近工业实际应用场景,提供的技术资料完整翔实,任务要求清晰明确,难度设置也十分适合开发者进行技能提升。希望后续可以增加更多贴近工业实际应用场景的活动。