一、所选任务介绍
本次电子森林活动所选任务为:基于ADMT4000磁传感器模块,设计并实现一个2维水平转台(模拟炮塔定向功能)控制系统,核心任务是将ADMT4000模块的46圈角度范围精准映射为0°~360°,实现转台的高分辨率方位定位,同时新增激光跟随人脸功能,无需实现复杂目标检测,重点演示转台定向与激光跟随的联动功能,贴合电子森林“科技+互动”的核心调性。任务核心要求包括:完成46圈→0°~360°的高精度映射、断电重启后角度状态保持一致、移动状态下锁定目标角度、清晰实现多圈到单圈的映射方案;额外实现激光笔跟随人脸功能,通过K230摄像头检测人脸坐标,联动转台调整激光指向,增强互动体验。
二、项目描述
本次电子森林活动项目为2维水平转台控制系统(模拟炮塔定向功能,新增激光跟随人脸功能),核心功能是通过ADMT4000磁传感器模块采集步进电机旋转角度,将模块46圈的角度范围精准映射为0°~360°,实现转台的高精度方位定位、指定角度定向,同时支持断电记忆角度状态、移动状态下锁定目标角度,新增核心功能为激光笔跟随人脸——通过K230摄像头检测人脸并输出坐标数据,经ESP8266 WLAN模块传输至主控,联动转台调整激光指向,适配电子森林活动的科技互动展示需求。项目设计初衷是结合电子技术,展现高精度控制与视觉互动的科技魅力,贴合电子森林“科技赋能、趣味互动”的理念,无需实现复杂目标跟踪,仅专注于角度感知、控制精度及激光跟随的直观呈现。项目应用场景为电子森林活动现场桌面展示,可通过按键操作设定角度、切换界面,演示转台定向与激光跟随功能,搭配激光笔模拟指向,增强展示的直观性;通过两个ESP8266 WLAN模块(分别安装在动、静平台),以手机热点为中介实现双向通信,传输K230摄像头检测到的人脸框选中心坐标及矩形长宽数据,为激光跟随提供数据支撑。项目预期达成效果:实现46圈→0°~360°的高精度映射,定位误差控制在合理范围,断电重启后角度状态保持一致,按键操作流畅,转台定向精准;K230摄像头可稳定检测人脸并输出坐标,激光笔可跟随人脸移动调整指向,ESP8266通信稳定。
三、芯片选型 / 硬件介绍
结合本次转台控制系统的功能需求(角度采集、高精度控制、通信联动、显示与操作),贴合电子森林活动现场展示的便捷性,硬件设计以实用性、稳定性为核心,无需过度复杂,重点保障角度感知与控制精度,以下为详细硬件配置说明。
3.1 核心芯片
本次项目选用STM32F103C8T6单片机作为主控芯片,选型原因如下:该芯片性价比高、功耗适中,引脚资源充足,可满足ADMT4000模块的SPI通信、ESP8266模块的串口通信、步进电机的驱动控制、OLED屏幕的软件模拟IIC通信以及6个按键的输入检测,操作便捷、调试简单,适配活动展示所需的稳定运行需求,且开发资料丰富,便于快速实现核心功能。该芯片在项目中的核心作用的:作为整个系统的控制中枢,接收ADMT4000模块采集的角度、圈数数据,处理按键输入信号,控制步进电机的转动角度与速度,驱动OLED屏幕显示相关参数,通过串口与ESP8266 WLAN模块通信,实现数据传输与指令下发,同时存储角度状态,保障断电重启后角度一致。
3.2 硬件组成
本次项目硬件组成围绕2维水平平台(静平台+动平台)展开,各硬件元件及作用如下,贴合转台控制、激光跟随功能与电子森林展示需求,硬件搭配合理、逻辑清晰:
- 核心模块:ADI ADMT4000磁传感器模块,与步进电机背后的磁体正对安装,通过SPI通信与STM32主控连接,核心作用是采集步进电机的旋转圈数与角度数据,为46圈→0°~360°的映射提供原始数据,配备复位按键,可重置角度采集状态;尤为关键的是,该模块可实时反馈转台实际旋转角度,针对动平台上12V聚合物电池过重导致的惯性问题(如旋转180°时易多转20°左右),形成完美的闭环控制,校正惯性过冲偏差,保障定位精度;

- 主控相关:STM32F103C8T6单片机、单片机扩展板,扩展板用于扩展引脚,承载OLED屏幕与按键,为各模块提供稳定的安装与连接接口;

- 显示与操作:0.96英寸单色OLED屏幕,采用软件模拟IIC通信,SCL引脚为PB3、SDA引脚为PB4,用于显示ADMT4000读取的圈数、角度,以及各操作界面的参数;6个按键(引脚分别为PA11、PB8、PB12、PB13、PB14、PB15),功能明确分工:界面切换键:切换3个显示界面(界面1:ADMT4000读取的圈数、角度;界面2:固定旋转角度选项,含45°、90°、180°、360°及正反方向,配备确认OK键;界面3:自定义参数选项,含待旋转角度、圈数、速度、方向,以及每次参数增减的绝对值,配备OK确认键);光标移动键:控制界面内光标移动(显示为小三角形);参数调节键:实现参数的增加、减小;确认键:仅在界面2中有效,用于确认选择的固定角度并执行定向操作;ADMT4000复位键:重置ADMT4000的角度采集状态;



- 动力驱动:42步进电机(静平台安装)、L298N电机驱动板,STM32通过PB6、PB7、PB8、PB9引脚向L298N输出信号,驱动步进电机以1/8拍方式转动,步进电机旋转轴上安装磁体,与ADMT4000模块配合实现角度采集,同时通过减速齿轮组将电机多圈转动转为转台单圈转动,辅助实现多圈→单圈映射;

- 电源模块:12V聚合物电池组(动平台安装)、降压模块(输入12V,输出5V、3.3V,实际仅使用5V),12V电池通过DC底座连接扩展板,一方面经降压模块输出5V,为激光笔、K230摄像头、ESP8266 WLAN模块供电,另一方面直接为L298N驱动板和步进电机供电;需特别说明的是,动平台上的12V聚合物电池重量较大,是导致转台旋转固定角度(如180°)时惯性过大、易过冲20°左右的主要原因,需通过ADMT4000的闭环反馈实现偏差校正;


- 通信模块:2个ESP8266 WLAN模块(分别安装在动平台与静平台),采用串口通信(静平台模块TXD为PA9、RXD为PA10),均连接手机热点实现双向通信,核心用于传输K230摄像头检测到的人脸框选中心坐标及矩形长宽数据,为激光跟随功能提供数据支撑
- 视觉与跟随模块:K230摄像头(动平台安装),核心作用是检测人脸并框选,通过串口通信将人脸框选中心坐标、矩形长宽数据传输至动平台的ESP8266模块;激光笔(动平台安装,模拟指向,增强展示直观性),由5V电源供电,其指向角度随转台转动同步调整,实现跟随人脸功能;

- 辅助部件:2维水平转台平台通过3D打印制作,分为静平台与动平台,静平台安装步进电机、ADMT4000模块、STM32单片机及扩展板;动平台安装12V聚合物电池、K230摄像头、ESP8266模块及激光笔;
- 其他:导线若干,用于各硬件之间的电路连接,保障信号与电源传输稳定,避免信号干扰影响角度采集与通信稳定性。
四、方案框图 + 设计思路
4.1 系统方案框图

4.2 设计思路
围绕电子森林活动“科技互动、直观展示”的主题,结合转台控制系统的核心需求(角度映射、定向控制、激光跟随),整体设计思路如下,重点突出ADMT4000多圈→单圈映射方案与闭环控制功能,确保角度控制精度与激光跟随的流畅性,同时解决动平台电池过重导致的惯性过冲问题:
需求定位:
明确项目核心是实现ADMT4000模块46圈角度到0°~360°的高精度映射,完成转台定向演示,新增激光跟随人脸功能,无需复杂目标跟踪,重点保障角度感知、控制精度及激光跟随效果;同时需解决动平台12V聚合物电池过重导致的惯性问题——旋转固定角度(如180°)时易多转20°左右,通过ADMT4000的角度反馈形成闭环控制,校正偏差;
硬件设计思路:
根据功能需求选型硬件,采用STM32F103C8T6作为主控,搭配ADMT4000磁传感器采集角度数据,步进电机提供动力,L298N驱动电机,OLED屏幕实现参数显示,按键提供操作接口,ESP8266模块实现动、静平台通信,K230摄像头实现人脸检测,12V电池+降压模块提供稳定电源;将硬件分为静平台与动平台布局,静平台负责角度采集、电机控制、指令处理,动平台负责人脸检测、激光发射与数据传输,激光笔模拟指向,增强展示效果,硬件布局贴合2维水平转台的结构需求;需重点考虑动平台电池重量带来的惯性影响,依托ADMT4000的实时角度反馈,设计闭环控制逻辑;
核心闭环控制设计(解决惯性过冲问题):
针对动平台电池过重导致的惯性问题,设计基于ADMT4000的闭环控制逻辑,核心逻辑如下:当转台旋转固定角度(如180°)时,STM32下发转动指令后,步进电机带动转台转动,由于惯性作用,转台易超出目标角度约20°;此时ADMT4000实时采集转台实际角度,将数据反馈至STM32主控,主控对比实际角度与目标角度的偏差,立即下发反向微调指令,控制步进电机反向转动,校正过冲偏差,形成“指令下发→电机转动→角度反馈→偏差校正”的完美闭环,最终将定位精度控制在±0.9°,该精度偏差主要由42步进电机自身精度决定,与闭环控制逻辑无关;
核心新增功能——激光跟随人脸设计:
- 数据采集:K230摄像头实时检测人脸,框选人脸区域并计算中心像素点坐标、矩形长宽数据,通过串口通信将数据传输至动平台的ESP8266模块;
- 数据传输:两个ESP8266模块通过手机热点建立双向通信,动平台ESP8266将人脸坐标数据传输至静平台ESP8266,再由静平台ESP8266通过串口传输至STM32主控;
- 角度计算与控制:STM32主控接收人脸坐标数据,结合转台当前映射角度,计算出使人脸处于激光指向范围内的目标角度,下发指令控制步进电机转动,带动转台及动平台上的激光笔同步转动,实现激光跟随人脸功能;
辅助功能设计:
- 断电角度保持:利用ADMT4000模块的掉电记忆功能;
- 移动状态锁定目标角度:在步进电机转动(转台移动)过程中,STM32实时对比当前映射角度与目标角度(按键设定角度或人脸坐标对应角度),通过PID算法调节步进电机转速,当接近目标角度时减速,到达目标角度后立即停止,结合闭环校正,进一步提升锁定精度;
- 界面与操作设计:严格按照6个按键功能,设计3个OLED显示界面,界面1显示ADMT4000读取的圈数、角度;界面2提供45°、90°、180°、360°固定角度选项及正反方向,支持确认操作;界面3支持自定义待旋转角度、圈数、速度、方向及参数增减幅度,支持确认操作,通过光标移动键切换参数选项,适配活动现场的便捷演示;
整体逻辑:
开机后ADMT4000复位,采集初始角度与圈数,STM32完成映射计算,OLED显示初始参数;用户可通过按键操作切换界面、设定目标角度,STM32根据目标角度控制步进电机转动,ADMT4000实时反馈角度数据,主控实时校正惯性过冲偏差,确保转台精准定向(精度±0.9°);同时K230摄像头实时检测人脸,传输坐标数据,STM32根据坐标数据调整转台角度,实现激光跟随,ESP8266保持通信稳定,完成演示功能。
五、原理图、PCB 设计
电源框图

pcb
六、软件流程图 + 调试软件说明 + 关键代码说明
6.1 软件流程图

6.2通信流程图

6.3 调试软件说明
本次项目调试所用软件及调试流程,贴合硬件、软件功能需求,重点覆盖角度采集、电机控制、激光跟随、通信联动等核心环节,确保调试流程可复现、问题可排查,具体说明如下:
1.核心调试软件:
- Keil uVision5:用于STM32F103C8T6单片机的程序编写、编译、下载与调试,核心用于调试软件逻辑(角度映射算法、电机控制逻辑、按键响应逻辑、ESP8266通信逻辑、激光跟随逻辑),可通过断点调试排查程序报错、逻辑漏洞;
- 串口调试助手:用于调试串口通信,包括STM32与ESP8266的串口通信、K230摄像头与动平台ESP8266的串口通信,可实时查看传输的数据(人脸坐标、角度数据),排查通信波特率不匹配、数据传输丢失等问题;
- ESP8266调试工具:用于配置两个ESP8266模块的热点连接参数(SSID、密码),确保两个模块能成功连接同一手机热点,实现双向通信;
- K230摄像头调试工具:用于调试K230的人脸检测功能,校准人脸坐标输出精度,调整检测参数(如检测灵敏度),确保能稳定框选人脸并输出准确坐标;
2. 调试核心目的:排查硬件连接错误(如引脚接反、导线接触不良)、修正软件逻辑漏洞(如角度映射误差、电机转动方向错误、激光跟随不流畅)、优化控制精度(如角度定位误差、激光跟随响应速度)、保障通信稳定(如ESP8266数据传输不丢失、K230坐标传输准确),确保项目能稳定实现核心功能,适配电子森林活动现场演示;
3. 调试操作步骤:
- 硬件调试:先单独测试各模块供电是否正常(12V、5V电源输出),再测试各模块连接是否正常(ADMT4000与STM32的SPI通信、ESP8266与STM32的串口通信、K230与ESP8266的串口通信、按键与OLED的连接),确保各模块能正常工作;
- 软件分模块调试:先调试ADMT4000角度采集功能,确保能准确读取圈数与原始角度;再调试角度映射算法,验证46圈→0°~360°的映射精度;接着调试步进电机控制,确保电机转动方向、速度正常,能实现目标角度锁定;然后调试按键与OLED界面,确保界面切换、参数调节、确认操作流畅;最后调试K230人脸检测与ESP8266通信,实现激光跟随功能;
- 整体联调:将所有模块整合,测试定向功能、激光跟随功能、断电记忆功能的联动效果,排查各模块协同工作时的问题(如通信干扰导致激光跟随卡顿);
4. 调试注意事项:调试时需断开不必要的电源,避免短路损坏硬件;调试角度映射时,需多次测试不同圈数对应的映射角度,校准算法误差;调试激光跟随时,需调 整K230摄像头的安装角度与检测灵敏度,确保人脸检测稳定;调试ESP8266通信时,需确保两个模块连接同一热点,通信波特率一致,避免数据传输异常。
6.3 关键代码
读取ATMD4000数据 解析角度 圈数
/* ============================================================
* CRC5 计算
* ============================================================ */
uint8_t admt4000_crc5(uint32_t data) {
uint8_t crc = 0x1F; // 初始值:全1
for (int i = 30; i >= 5; i--) {
uint8_t input_bit = ((data >> i) & 0x01);
uint8_t feedback = ((crc >> 4) & 0x01) ^ input_bit;
crc = (crc << 1) & 0x1E; // 左移1位,保持低5位
if (feedback) {
crc ^= 0x05; // 多项式 x^2 + x^0 (二进制: 00101)
}
}
return crc & 0x1F; // 返回低5位
}
uint8_t admt4000_read_registers_dual(uint8_t reg1, uint8_t reg2,
uint16_t *data1, uint16_t *data2) {
uint8_t tx[8];
uint8_t rx[8];
// 构造第一个寄存器读取命令
tx[0] = 0x80 | (reg1 & 0x3F);
tx[1] = 0;
tx[2] = 0;
tx[3] = 0;
// 构造第二个寄存器读取命令
tx[4] = 0x80 | (reg2 & 0x3F);
tx[5] = 0;
tx[6] = 0;
tx[7] = 0;
SPI_TransmitReceiveBytes(&hspi1,tx,rx,8);
// 解析第一个寄存器数据
uint16_t reg1_data = ((uint16_t)rx[1] << 8) | rx[2];
bool reg1_valid = (rx[3] & 0x80) != 0;
uint8_t recv_crc1 = rx[3] & 0x1F;
uint32_t crc_data1 = ((uint32_t)tx[0] << 24) | ((uint32_t)rx[1] << 16) |
((uint32_t)rx[2] << 8) | rx[3];
uint8_t calc_crc1 = admt4000_crc5(crc_data1);
// 解析第二个寄存器数据
uint16_t reg2_data = ((uint16_t)rx[5] << 8) | rx[6];
bool reg2_valid = (rx[7] & 0x80) != 0;
uint8_t recv_crc2 = rx[7] & 0x1F;
uint32_t crc_data2 = ((uint32_t)tx[4] << 24) | ((uint32_t)rx[5] << 16) |
((uint32_t)rx[6] << 8) | rx[7];
uint8_t calc_crc2 = admt4000_crc5(crc_data2);
if (data1 != NULL) *data1 = reg1_data;
if (data2 != NULL) *data2 = reg2_data;
bool crc1_ok = (recv_crc1 == calc_crc1);
bool crc2_ok = (recv_crc2 == calc_crc2);
return 0;
}
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;
}
// 检查是否为两==9码负数 (>= 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) {
printf("ADMT4000 qs max: %d (max:46)", turn_count);
return -998.0f; // 表示超出范围
}
// 正常正数圈数 + 小数部分
return (float)turn_count + (float)single_turn_angle / 1024.0f;
}
/* ============================================================
* 角度转换
* ============================================================ */
float admt4000_convert_to_angle(uint16_t raw_value) {
// 提取角度值 [15:4]
uint16_t angle_value = (raw_value >> 4) & 0xFFF;
return (float)angle_value * 360.0f / 4096.0f;
}
float admt4000_extract_single_turn_angle(uint16_t raw_value) {
// 提取单圈角度值 [9:0]
uint16_t angle_value = raw_value & 0x3FF;
return (float)angle_value * 0.351f;
}
摄像头跟踪
void host(void)
{
// 1. 无效数据过滤:识别不到目标时停止+清零积分
if (sxt_data[0] == 0 && sxt_data[1] == 0 && sxt_data[2] == 0 && sxt_data[3] == 0)
{
motor_state = 0;
integral = 0.0f;
return;
}
// 2. 防抖延时:旋转完成后暂停,避免频繁触发
if (HAL_GetTick() - last_rotate_time < ANTI_SHAKE_DELAY)
{
return;
}
// 3. 电机运行保护:正在旋转时不重复设置
if (motor_running == 1)
{
return;
}
// 4. 读取ADMT4000实时角度(核心:加入角度传感器)
ADMT_read(); // 替换为你实际的读角度函数
// 5. 计算矩形中心X坐标
float rect_center_x = (float)sxt_data[0] + (float)sxt_data[2] / 2.0f;
// 6. 计算像素偏移(相对画面中心)
float offset_pixels = rect_center_x - SCREEN_CENTER_X;
// 7. 关键修复1:限制像素偏移范围,避免转出画面
if (offset_pixels > MAX_OFFSET_PIXELS) offset_pixels = MAX_OFFSET_PIXELS;
if (offset_pixels < -MAX_OFFSET_PIXELS) offset_pixels = -MAX_OFFSET_PIXELS;
// 8. 死区判断:偏差太小则停止+清零积分
if (fabsf(offset_pixels) < DEAD_ZONE_PIXELS)
{
motor_state = 0;
integral = 0.0f;
return;
}
// 9. PID参数(保留你的框架,加入角度修正)
float kp = PIXEL_TO_PAI;
float ki = 0.001f;
// 新增:用ADMT4000角度修正像素偏移(提升精度)
// 像素偏移转角度偏移,和ADMT4000反馈做闭环
float offset_angle = offset_pixels * PIXEL_TO_PAI * STEPS_PER_DEGREE;
float angle_error = offset_angle - (admt4000_angle - (int)admt4000_angle); // 角度残差修正
// 10. 积分项处理(加入角度误差修正,避免积分跑偏)
integral += (offset_pixels + angle_error * 10); // 角度误差放大后参与积分,提升精度
if (integral > INTEGRAL_LIMIT) integral = INTEGRAL_LIMIT;
if (integral < -INTEGRAL_LIMIT) integral = -INTEGRAL_LIMIT;
// 11. 计算目标拍数(保留你的逻辑,加双保险限幅)
float target_pai_float = (kp * offset_pixels) + (ki * integral);
int16_t target_pai_int = (int16_t)target_pai_float;
// 12. 关键修复2:双重限幅,绝对不让转太多
// ① 拍数限幅(单次最多转30拍)
if (target_pai_int > MAX_PAI_PER_TIME) target_pai_int = MAX_PAI_PER_TIME;
if (target_pai_int < -MAX_PAI_PER_TIME) target_pai_int = -MAX_PAI_PER_TIME;
// ② 拍数对应像素偏移不超画面(防止转出)
float pai_to_pixel = target_pai_int / PIXEL_TO_PAI;
if (fabsf(pai_to_pixel) > MAX_OFFSET_PIXELS)
{
target_pai_int = (int16_t)(MAX_OFFSET_PIXELS * PIXEL_TO_PAI * (target_pai_int > 0 ? 1 : -1));
}
// 13. 方向判断
uint8_t dir = (target_pai_int > 0) ? 1 : 0;
//printf("x:%d\n\r",target_pai_int); // 保留你的调试打印
// 14. 配置电机参数(速度调慢,减少超调转出)
motor_state = 1;
motor_direction = dir;
motor_pai = (uint16_t)abs(target_pai_int);
motor_speed = 8; // 从5调到8,转得更慢,避免转出画面(值越大越慢)
}
反馈闭环
// 更新旋转数据
void make_up(uint32_t pai)
{
// 计算旋转后的预期角度(模360)
float angle_change = ((float)pai * 360.0f) / 400.0f;
target_angle = admt4000_angle + angle_change;
// 归一化到 [0, 360)
while (target_angle >= 360.0f) target_angle -= 360.0f;
while (target_angle < 0.0f) target_angle += 360.0f;
printf("target_angle:%.2f\r\n",target_angle);
}
// 实施旋转闭环
void make_up_up(void)
{
// 计算实际角度与目标角度的差值(考虑环形,取最短路径)
float diff = target_angle - admt4000_angle;
if (diff >= 180.0f) diff -= 360.0f;
if (diff < -180.0f) diff += 360.0f;
printf("admt4000:%.2f\r\n",admt4000_angle);
printf("diff:%.2f\r\n",diff);
// 若差值超过阈值,启动补偿旋转
if (fabs(diff) > 0.9f)
{
// 计算补偿所需步数(拍数),至少1步
uint16_t steps = (uint16_t)(fabs(diff) * 400.0f / 360.0f);
if (steps == 0) steps = 1;
if (steps > 255) steps = 255; // 限制在 uint8_t 范围内
printf("steps:%.d\r\n",steps);
// 设置补偿参数
motor_circle = 0;
motor_pai = (uint8_t)steps;
motor_direction = (diff > 0) ? 0 : 1; // 方向0为正转(角度增加),1为反转
motor_speed = 20; // 补偿时慢速旋转
motor_state = 1; // 启动电机
compensating = 1; // 标记进入补偿状态
}
else
{
numd_bos_f=0;
compensating = 0; // 无需补偿或补偿完成,清除标志
}
}
void motor_control(void) //步进电机旋转
{
static uint8_t phase_index = 0;
static uint32_t step_count = 0;
static uint32_t target_steps = 0;
static uint16_t last_circle = 0, last_pai = 0;
static uint8_t step_divider = 0;
if (motor_state == 0) {
motorA_writer(0); motorB_writer(0);
motorC_writer(0); motorD_writer(0);
motor_running = 0;
return;
}
// 参数变化时,若不在补偿状态则重新计算目标角度
if (motor_circle != last_circle || motor_pai != last_pai||motor_running == 0) {
last_circle = motor_circle;
last_pai = motor_pai;
target_steps = (uint32_t)motor_circle * 400 + motor_pai;
if(numd_bos)
{
numd_bos=0;
make_up(target_steps); // 计算目标角度
}
step_count = 0;
}
if (motor_speed == 0 || motor_speed > 50) {
motorA_writer(0); motorB_writer(0);
motorC_writer(0); motorD_writer(0);
motor_running = 0;
return;
}
step_divider++;
if (step_divider < motor_speed) {
motor_running = 1;
return;
}
step_divider = 0;
// 旋转完成
if (target_steps > 0 && step_count >= target_steps) {
motor_state = 0;
motorA_writer(0); motorB_writer(0);
motorC_writer(0); motorD_writer(0);
motor_running = 0;
last_rotate_time = HAL_GetTick(); // 记录完成时间
if(numd_bos_f)compensating=1; //compensating==1 还没有补偿到位
printf("admt4000_angle:%.2f\n\r",admt4000_angle);
return;
}
// 步进电机相位更新
if (motor_direction == 0)
phase_index = (phase_index + 1) % 8;
else
phase_index = (phase_index + 7) % 8;
motorA_writer(phase_table[phase_index][0]);
motorB_writer(phase_table[phase_index][1]);
motorC_writer(phase_table[phase_index][2]);
motorD_writer(phase_table[phase_index][3]);
step_count++;
motor_running = 1;
}
K230人脸识别
from libs.PipeLine import PipeLine, ScopedTiming
from libs.AIBase import AIBase
from libs.AI2D import Ai2d
import os
import ujson
from media.media import *
from time import *
import nncase_runtime as nn
import ulab.numpy as np
import time
import utime
import image
import random
import gc
import sys
import aidemo
#串口初始化部分
from machine import UART
from machine import FPIOA
# 配置引脚
fpioa = FPIOA()
fpioa.set_function(11, FPIOA.UART2_TXD)
fpioa.set_function(12, FPIOA.UART2_RXD)
# 初始化UART2,波特率115200,8位数据位,无校验,1位停止位
uart = UART(UART.UART2, baudrate=115200, bits=UART.EIGHTBITS,
parity=UART.PARITY_NONE, stop=UART.STOPBITS_ONE)
def send_at_cmd(cmd):
"""发送 AT 命令"""
uart.write(cmd + "\r\n")
def at_init():
"""发送 AT 指令序列"""
print("AT init start...")
# 1. 重启模块
send_at_cmd("ATE0") # 关闭回显
time.sleep_ms(200)
send_at_cmd("AT+RST")
time.sleep_ms(2000)
# 2. 设置为 STA 模式
send_at_cmd("AT+CWMODE=1")
time.sleep_ms(500)
# 3. 连接 WiFi(请根据实际修改 SSID 和密码)
ssid = "smac"
pwd = "012345678"
send_at_cmd('AT+CWJAP="{}","{}"'.format(ssid, pwd))
time.sleep_ms(10000) # 等待连接
# 4. 建立 TCP 连接
server_ip = "192.168.151.30"
server_port = "8080"
send_at_cmd('AT+CIPSTART="TCP","{}",{}'.format(server_ip, server_port))
time.sleep_ms(3000)
# 5. 开启透传模式
send_at_cmd("AT+CIPMODE=1")
time.sleep_ms(500)
# 6. 进入透传模式
send_at_cmd("AT+CIPSEND")
time.sleep_ms(500)
print("AT init commands sent.")
# 自定义人脸检测类,继承自AIBase基类
class FaceDetectionApp(AIBase):
def __init__(self, kmodel_path, model_input_size, anchors, confidence_threshold=0.5, nms_threshold=0.2, rgb888p_size=[224,224], display_size=[1920,1080], debug_mode=0):
super().__init__(kmodel_path, model_input_size, rgb888p_size, debug_mode)
self.kmodel_path = kmodel_path
self.model_input_size = model_input_size
self.confidence_threshold = confidence_threshold
self.nms_threshold = nms_threshold
self.anchors = anchors
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]]
self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]]
self.debug_mode = debug_mode
self.ai2d = Ai2d(debug_mode)
self.ai2d.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT, nn.ai2d_format.NCHW_FMT, np.uint8, np.uint8)
def config_preprocess(self, input_image_size=None):
with ScopedTiming("set preprocess config", self.debug_mode > 0):
ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size
top, bottom, left, right = self.get_padding_param()
self.ai2d.pad([0, 0, 0, 0, top, bottom, left, right], 0, [104, 117, 123])
self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
self.ai2d.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,self.model_input_size[1],self.model_input_size[0]])
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
post_ret = aidemo.face_det_post_process(self.confidence_threshold, self.nms_threshold,
self.model_input_size[1], self.anchors,
self.rgb888p_size, results)
if len(post_ret) == 0:
return post_ret
else:
return post_ret[0]
def draw_result(self, pl, dets):
with ScopedTiming("display_draw", self.debug_mode > 0):
if dets:
pl.osd_img.clear()
for det in dets:
x, y, w, h = map(lambda x: int(round(x, 0)), det[:4])
x = x * self.display_size[0] // self.rgb888p_size[0]
y = y * self.display_size[1] // self.rgb888p_size[1]
w = w * self.display_size[0] // self.rgb888p_size[0]
h = h * self.display_size[1] // self.rgb888p_size[1]
pl.osd_img.draw_rectangle(x, y, w, h, color=(255, 255, 0, 255), thickness=2)
else:
pl.osd_img.clear()
def get_padding_param(self):
dst_w = self.model_input_size[0]
dst_h = self.model_input_size[1]
ratio_w = dst_w / self.rgb888p_size[0]
ratio_h = dst_h / self.rgb888p_size[1]
ratio = min(ratio_w, ratio_h)
new_w = int(ratio * self.rgb888p_size[0])
new_h = int(ratio * self.rgb888p_size[1])
dw = (dst_w - new_w) / 2
dh = (dst_h - new_h) / 2
top = int(round(0))
bottom = int(round(dh * 2 + 0.1))
left = int(round(0))
right = int(round(dw * 2 - 0.1))
return top, bottom, left, right
if __name__ == "__main__":
display_mode = "lcd"
rgb888p_size = [1920, 1080]
if display_mode == "hdmi":
display_size = [1920,1080]
else:
display_size = [800,480]
# 设置模型路径和其他参数
kmodel_path = "/sdcard/examples/kmodel/face_detection_320.kmodel"
confidence_threshold = 0.5
nms_threshold = 0.2
anchor_len = 4200
det_dim = 4
anchors_path = "/sdcard/examples/utils/prior_data_320.bin"
anchors = np.fromfile(anchors_path, dtype=np.float)
anchors = anchors.reshape((anchor_len, det_dim))
# 初始化PipeLine
pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode)
pl.create()
# 初始化人脸检测实例
face_det = FaceDetectionApp(kmodel_path, model_input_size=[320, 320], anchors=anchors,
confidence_threshold=confidence_threshold, nms_threshold=nms_threshold,
rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0)
face_det.config_preprocess()
at_init()
try:
while True:
time.sleep_ms(70)
os.exitpoint()
with ScopedTiming("total", 1):
img = pl.get_frame()
res = face_det.run(img)
face_det.draw_result(pl, res)
if res:
# 画面中心坐标(原始rgb888p_size分辨率下)
screen_center_x = rgb888p_size[0] / 2.0
screen_center_y = rgb888p_size[1] / 2.0
for det in res:
# 提取检测框信息(坐标已在rgb888p_size分辨率下)
x, y, w, h = det[:4]
# 计算框的中心
center_x = x + w / 2.0
center_y = y + h / 2.0
# 相对于画面中心的偏移
dx = center_x - screen_center_x
dy = center_y - screen_center_y
# 构造发送字符串
message = "FACE:{:.1f},{:.1f},{:.1f},{:.1f}\n".format(
center_x, center_y, w, h)
uart.write(message)
pl.show_image()
gc.collect()
except Exception as e:
sys.print_exception(e)
finally:
face_det.deinit()
pl.destroy()
# uart.deinit()
七、实物演示及说明
1. 实物制作完成情况:
实物为2维水平转台(静平台+动平台),整体采用3D打印件搭建,外观简洁大方,贴合科技主题,无多余装饰;静平台固定安装STM32F103C8T6单片机、扩展板、42步进电机、ADMT4000磁传感器模块、L298N电机驱动板,步进电机旋转轴上安装磁体,ADMT4000模块正对磁体固定,确保角度采集精准;动平台安装12V聚合物电池组、降压模块、K230摄像头、ESP8266 WLAN模块及激光笔,激光笔固定于动平台边缘,模拟指向,角度随转台转动同步调整;需特别说明的是,动平台上的12V聚合物电池重量较大,是导致转台旋转固定角度(如180°)时惯性过大、易过冲20°左右的主要原因,通过ADMT4000的闭环反馈的实现了偏差校正;各硬件连接规范,导线整理整齐,避免信号干扰,DC底座固定于扩展板,便于电池供电,整体实物运行稳定,可满足活动现场长时间演示需求。



2. 演示步骤与操作方法:
演示前准备:将两个ESP8266模块连接同一手机热点,确保通信稳定;给动平台12V电池供电,扩展板通过DC底座接入电源,开机后ADMT4000自动复位,OLED显示界面1(ADMT4000读取的圈数、映射后的角度),K230摄像头启动,开始检测人脸;具体演示步骤:
- 转台定向功能演示(重点展示闭环控制效果):通过按键切换界面(界面切换键),切换至界面2(固定角度选项),选择180°(最易出现惯性过冲的角度)及转动方向,按下确认键,转台开始转动,由于动平台电池惯性,转台会短暂超出180°约20°,此时ADMT4000实时反馈实际角度,闭环控制立即启动,步进电机反向微调,快速将角度校正至180°左右,最终定位精度控制在±0.9°;同样可演示45°、90°、360°固定角度,均能通过闭环控制校正惯性过冲;切换至界面3(自定义参数),通过光标移动键选择待旋转角度、圈数、速度、方向及参数增减幅度,按下确认键,转台按设定参数转动,闭环控制同步工作,确保定位精准;



- 激光跟随功能演示:保持界面1显示,让人脸处于K230摄像头的检测范围内,摄像头将实时框选人脸,通过ESP8266模块传输人脸坐标数据,转台将自动调整角度,带动激光笔跟随人脸移动,激光点始终指向人,实现精准跟随;
- 断电记忆功能演示:在任意角度状态下断电,重新开机后,OLED将显示断电前的角度状态,转台保持当前角度不变,无需重新设定;


3. 演示效果说明:
重点突出闭环控制效果:转台旋转固定角度(如180°)时,成功解决动平台电池过重导致的20°左右惯性过冲问题,通过ADMT4000的实时角度反馈与闭环校正,定位精度稳定在±0.9°,该精度偏差由42步进电机自身精度决定,已达到最优控制效果;46圈→0°~360°映射精准,移动状态下角度锁定稳定,无明显晃动;激光跟随响应迅速,人脸移动时,转台可快速调整角度,激光点始终对准人脸中心,无明显延迟;OLED屏幕显示清晰,3个界面切换流畅,按键操作灵敏,无误触发;ESP8266通信稳定,人脸坐标数据传输无丢失,整体演示效果直观,能充分展现高精度角度控制、闭环校正与视觉互动的科技魅力,适配活动现场的展示与互动需求。
八、遇到的难点及解决方法
难点一:ADMT4000 磁传感器复位不稳定,偶尔复位失败
解决方法:
- 硬件调整:原 ADMT4000 芯片与旋转磁体距离过近,导致磁场干扰复位逻辑,将两者间距调整至8mm,消除强磁干扰;
- 软件优化:修改复位时序,复位前先将 RST 引脚拉低,保持稳定后执行复位操作,最后将 RST 引脚拉高,完成标准复位流程。
优化效果:ADMT4000 复位功能稳定可靠,无失败、无异常,开机与手动复位均可正常执行。
难点二:动平台电池重量大,旋转 180° 时惯性过冲约 20°,定位精度差
解决方法:
采用ADMT4000 角度实时反馈 + 步进电机闭环控制方案:
转台到达目标角度后,ADMT4000 立即采集实际角度,与目标角度对比计算偏差;
若角度误差超过 ±0.9°(步进电机固有精度限制),STM32 自动控制电机反向慢速微调,直至误差回到允许范围内。
优化效果:彻底解决惯性过冲问题,转台定位精度稳定达到 **±0.9°**,满足高精度定向要求。
难点三:步进电机转动卡顿、抖动,角度锁定不稳定
解决方法:
- 修复 L298N 驱动板使能引脚连接错误,由 STM32 GPIO 精准控制电机启停,避免误触发;
- 将驱动方式从1/4 拍改为 1/8 拍,增加细分步数,大幅提升转动平滑性。
优化效果:电机运行平稳无卡顿、无抖动,角度锁定快速精准,转台工作状态稳定。
难点四:ESP8266 无线通信不稳定,数据丢失、延迟高,激光跟随失效
解决方法:
- 统一所有串口设备波特率为 115200bps,保证时序同步;
- 优化无线热点配置,提升连接稳定性;
- 精简数据格式,仅传输人脸中心 X 坐标、Y 坐标、框宽、框高;
- 增加数据校验机制,防止错误数据影响系统运行。
优化效果:通信稳定无丢包,数据延迟 **≤30ms**,激光跟随流畅、响应迅速、无卡顿。
九、心得体会
本次二维水平转台控制系统的制作全过程参与本次电子森林活动项目,让我将课堂所学的电子技术知识应用到实际实践中,更深刻体会到“科技+互动”的魅力,也收获了宝贵的实践经验与成长。在项目设计初期,我明确了任务核心的是ADMT4000多圈→单圈的高精度映射与转台定向,新增激光跟随功能,重点突出科技互动,这一定位让我在硬件选型、软件设计中始终围绕实用性、稳定性和互动性展开,避免了功能冗余。硬件搭建过程中,我从STM32F103C8T6主控的引脚分配、各模块的连接,到静平台与动平台的布局,每一步都需要细致严谨,尤其是ADMT4000模块与步进电机磁体的正对安装、ESP8266模块的热点连接,任何一个细节的疏忽都可能导致功能异常。通过反复调整硬件布局、整理导线、校准安装位置,我不仅掌握了各硬件模块的工作原理,更培养了严谨细致的实践态度。软件调试是本次项目的核心难点,从角度映射算法的优化、步进电机的平稳控制,到ESP8266的通信调试、激光跟随逻辑的实现,每一个环节都需要反复测试、不断修正。面对映射精度不足、电机卡顿、通信不稳定等问题,我学会了拆分问题、逐步排查,通过查阅资料、调整算法、优化参数,最终解决了所有难点,这一过程让我深刻认识到,实践中遇到的问题往往需要理论与实际结合,灵活变通才能找到解决方案。本次项目让我对电子技术的应用有了更深入的理解,也让我体会到电子森林活动的意义将科技与趣味、互动结合,让更多人感受科技的魅力。同时,我也认识到自身的不足,比如在算法优化、硬件抗干扰设计上还有提升空间,未来我会继续加强学习,不断提升自己的实践能力。通过本次项目,我不仅完成了活动任务,更收获了坚持与成长,也明白了科技的价值在于服务于趣味互动与生活,这也让我更加坚定了继续探索电子技术应用的决心,未来希望能设计出更多贴合科技与生活、兼具趣味性与实用性的作品。

