Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
NXP-MCXN947
步进电机
【24嵌入式设计大赛】半自动散料贴片机(待填坑)
发布于 2024-09-18 06:06:53 浏览:227
订阅该版
[tocm] # 项目预期 1. 控制CoreXY结构在XY平面上运动; 2. 在1所述结构上设计工具头,实现Z轴升降和R轴旋转,使用舵机夹住/释放芯片吸盘,以完成贴片任务; 3. 通过摄像头识别料区分界线,并根据料区内布置的Apriltag辨别物料种类; 4. 建立回归模型和分类模型,在料区拾起芯片前计算放置角度,在板区识别出匹配的焊盘位置和焊盘角度,调整芯片角度以恰好贴在焊盘上,并通过eIQ工具包部署在板上。 5. 通过以太网在电脑上对系统进行控制。 # 实现情况 由于FRDM-MCXN947开发板的以太网口资源与提供的SmartDMA摄像头接口存在冲突,无法在不对硬件进行改动的条件下实现,故没有完成视觉相关任务。 搭建了CoreXY结构的运动框架,购置了拓竹P1S 3D打印机,但在有限工具头体积、有限成本下没有设计出较好的Z-R运动方案,故只实现了XY方向上的运动控制。(确实没想到提前进组会这么忙......) 通过以太网进行控制已实现。 # 硬件连接 为了控制CoreXY结构在XY平面上运动,除了FRDM-MCXN947开发板外,还需要2个A4988模块控制结构中的2个步进电机。 A4988需要VDD、VMOT两个电源。其中VDD为芯片提供电源供应,需从开发板上接入3.3V;VMOT为步进电机提供电源,外接12V直流电源。 [MS2..MS0]控制步进电机的细分数,在此将2个步进电机都设置为8细分。 DIR脚控制步进电机旋转方向。 EN脚为A4988的使能脚,当输入低电平时驱动使能,输入高电平时不对控制信号响应。 STEP脚为脉冲输入脚。每当输入一个脉冲,步进电机沿着DIR脚所规定的方向旋转1步。 ![硬件连接](https://oss-club.rt-thread.org/uploads/20240918/f0d2b43fb355f5131d51b16c87255829.jpg) 引脚连接如下: ```plain STEPPER0 EN <------> P1_17 DIR <------> P1_1 STEP <------> P1_3 (PWM1,ch1) STEPPER1 EN <------> P3_22 DIR <------> P3_21 STEP <------> P1_12 (PWM2,ch2) POWER VDD <------> 3V3 VMOT <------> 12V ``` # 机械结构 CoreXY是一种常用于3D打印机、激光雕刻机等设备中定位系统的的机械结构设计。它以结构紧凑、效率高、易于组装和维护著称。 CoreXY系统包含2个电机、2条同步带。由于电机均布置在固定结构上,所以可以实现运动结构的轻量化,以达到高速运动的目的。 ![CoreXY简介](https://oss-club.rt-thread.org/uploads/20240918/a80d0b4f5473b397747ae31bf9a7ada1.png) 根据其独特的布置,X、Y两个轴向各自都是由两个电机共同控制的,这样不仅能对运动进行精准的控制,而且在单个轴向上运动时,能获得比单个电机驱动更强的力矩,也减少了振动。 这种结构具有空间利用率高、高精度、高效率的优点。然而,由于其复杂的皮带路径设计,使得它只适用于较小型的设备,否则随着设备尺寸增加,传动部件的成本将大幅提高。 ![搭建效果](https://oss-club.rt-thread.org/uploads/20240918/f3827d73ec242aa1ce59f9f00b08dbce.jpg) 我按照大鱼DIY提供的T10激光雕刻机,复刻组装了其定位系统的机械部分,使用了高精度切割的铝型材和线性导轨,与一般的光轴方案相比,精度更高、公差更小,运行起来丝滑噪音小。 # 控制逻辑 ## 步进电机的控制 步进电机使用A4988进行控制,整体控制思路是,输入单个步进电机的转动速度(含方向)和持续时间,设置一个单次触发的硬件定时器,同时开启PWM,按照转动速度对应的频率输出脉冲波形;当硬件定时器溢出时,调用电机对应的回调函数,关闭PWM,以此达到了既控制步进电机转动速度,又控制了移动位置(脉冲数)的目的。 主要代码如下: ```c /** @file stepper.h **/ #ifndef __STEPPER_H #define __STEPPER_H #include
#include
enum stepper_dir_t { STEPPER_DIR_CW, STEPPER_DIR_CCW }; enum stepper_en_t { STEPPER_EN_ENABLED, STEPPER_EN_DISABLED }; struct stepper_device_t { uint32_t dir; uint32_t en; volatile uint32_t motor_lock; uint32_t step_pin; uint32_t en_pin; uint32_t dir_pin; uint8_t timer_device_name[10]; rt_device_t timer_device; rt_err_t (*cb)(rt_device_t dev, rt_size_t size); uint8_t pwm_device_name[10]; rt_device_t pwm_device; int pwm_channel; }; extern struct stepper_device_t motor_x; extern struct stepper_device_t motor_y; void stepper_port_init(); void stepper_launch(struct stepper_device_t *ins, int32_t speed, uint32_t duration); void stepper_init(struct stepper_device_t *ins); void stepper_enable(struct stepper_device_t *ins); void stepper_disable(struct stepper_device_t *ins); void stepper_dir_set(struct stepper_device_t *ins, enum stepper_dir_t dir); rt_err_t motor_x_cb(rt_device_t dev, rt_size_t size); rt_err_t motor_y_cb(rt_device_t dev, rt_size_t size); #endif ``` ```c /** @file stepper.c **/ #include "stepper.h" #include "fsl_port.h" struct stepper_device_t motor_x = { .en_pin = (32 * 1) + 17, .dir_pin = (32 * 1) + 1, .step_pin = (32 * 1) + 3, .timer_device_name = "timer1", .pwm_device_name = "pwm1", .pwm_channel = 1, .cb = motor_x_cb}; struct stepper_device_t motor_y = { .en_pin = (32 * 3) + 22, .dir_pin = (32 * 3) + 21, .step_pin = (32 * 1) + 12, .timer_device_name = "timer2", .pwm_device_name = "pwm2", .pwm_channel = 2, .cb = motor_y_cb}; /** * @brief 对步进电机设备实例进行初始化 * * @param ins 步进电机实例 */ void stepper_init(struct stepper_device_t *ins) { // 1. 初始化相关外设和变量 rt_pin_mode(ins->en_pin, PIN_MODE_OUTPUT); rt_pin_mode(ins->dir_pin, PIN_MODE_OUTPUT); rt_pin_write(ins->en_pin, PIN_HIGH); rt_pin_write(ins->dir_pin, PIN_LOW); rt_hwtimerval_t timeout; rt_hwtimer_mode_t mode = HWTIMER_MODE_ONESHOT; rt_err_t res; uint32_t freq = 150000000; //在电机转动时加锁,当单次运动的全部锁被释放时,表示运动完成,退出阻塞 ins->motor_lock = 0; // 2. 初始化硬件定时器(HWTIMER) ins->timer_device = rt_device_find(ins->timer_device_name); if (ins->timer_device == NULL) rt_kprintf("hwtimer device not found.\r\n"); res = rt_device_open(ins->timer_device, RT_DEVICE_FLAG_RDWR); if (res != RT_EOK) rt_kprintf("open hwtimer failed=%d.\r\n", res); //绑定回调函数 rt_device_set_rx_indicate(ins->timer_device, ins->cb); if (res != RT_EOK) rt_kprintf("attatch callback error=%d.\r\n", res); rt_device_control(ins->timer_device, HWTIMER_CTRL_FREQ_SET, &freq); rt_device_control(ins->timer_device, HWTIMER_CTRL_MODE_SET, &mode); // 3. 初始化PWM外设 ins->pwm_device = rt_device_find(ins->pwm_device_name); if (ins->pwm_device == NULL) rt_kprintf("PWM device not found.\r\n"); } /** * @brief 启动一次运动 * * @param ins 步进电机实例 * @param speed 转动速度,单位:step/s. * @param duration 转动持续时间,us. */ void stepper_launch(struct stepper_device_t *ins, int32_t speed, uint32_t duration) { int32_t pulse_period = 1000000000 / abs(speed); rt_hwtimerval_t timeout; timeout.usec = duration % 1000000; timeout.sec = duration / 1000000; if (speed == 0 || duration == 0) { //若无需运动,则直接释放,退出 ins->motor_lock = 0; return; } //设置方向 if (speed > 0) stepper_dir_set(ins, STEPPER_DIR_CW); else stepper_dir_set(ins, STEPPER_DIR_CCW); //启动PWM波形输出 rt_pwm_set((struct rt_device_pwm *)ins->pwm_device, ins->pwm_channel, pulse_period, pulse_period / 2); rt_pwm_enable((struct rt_device_pwm *)ins->pwm_device, ins->pwm_channel); rt_device_write(ins->timer_device, 0, &timeout, sizeof(rt_hwtimerval_t)); } /** * @brief 配置PWM相关引脚的复用功能 */ void stepper_port_init() { PORT_SetPinMux(PORT1, 3, kPORT_MuxAlt4); PORT_SetPinMux(PORT1, 12, kPORT_MuxAlt4); } /** * @brief 使能步进电机,将EN脚拉低 * * @param ins 步进电机实例 **/ void stepper_enable(struct stepper_device_t *ins) { ins->en = STEPPER_EN_ENABLED; rt_pin_write(ins->en_pin, PIN_LOW); } /** * @brief 失能步进电机,将EN脚拉高 * * @param ins 步进电机实例 **/ void stepper_disable(struct stepper_device_t *ins) { ins->en = STEPPER_EN_DISABLED; rt_pin_write(ins->en_pin, PIN_HIGH); } /** * @brief 设置步进电机运动方向 * * @param ins 步进电机实例 * @param dir 运动方向 **/ void stepper_dir_set(struct stepper_device_t *ins, enum stepper_dir_t dir) { ins->dir = dir; if (dir == STEPPER_DIR_CW) rt_pin_write(ins->dir_pin, PIN_LOW); if (dir == STEPPER_DIR_CCW) rt_pin_write(ins->dir_pin, PIN_HIGH); } //回调函数,在硬件定时器溢出时执行,关闭PWM波形输出并释放锁。 rt_err_t motor_x_cb(rt_device_t dev, rt_size_t size) { rt_pwm_disable((struct rt_device_pwm *)motor_x.pwm_device, motor_x.pwm_channel); motor_x.motor_lock = 0; } rt_err_t motor_y_cb(rt_device_t dev, rt_size_t size) { rt_pwm_disable((struct rt_device_pwm *)motor_y.pwm_device, motor_y.pwm_channel); motor_y.motor_lock = 0; } ``` ## 工具头运动的控制 运动控制部分实现了相当于G-code中的G00/G01代码,即给定位置的点到点直线运动。 该部分记录了当前位置,并提供函数向指定位置以恒定速度运动。 在具体实现上,将点到点的运动分解成了X、Y方向上的速度,又根据CoreXY结构的特点计算出两电机的各自的运动参数(速度、时间),并将参数分发至步进电机控制程序,并阻塞,直至两电机都完成运动。 具体实施如下: ```c /** @file motion.h **/ #ifndef __MOTION_H #define __MOTION_H #include "rtdevice.h" #include "rtthread.h" #include "stepper.h" #define COREXY // #define XY #define STEP_PER_MM 80.0f struct vector3 { volatile float x; volatile float y; volatile float z; }; struct motion_info { struct vector3 position; //当前位置 struct vector3 velocity; //当前速度 float max_speed; // unit: mm/s //最大速度 struct stepper_device_t *stepper_x; //指向电机A实例 struct stepper_device_t *stepper_y; //指向电机B实例 volatile int32_t motion_lock; //电机占用锁 }; extern struct motion_info motion; void motion_goto(struct motion_info *ins, struct vector3 *dest); #endif ``` ```c /** @file motion.c **/ #include "motion.h" #include "math.h" struct motion_info motion = { .max_speed = 100, .position = { .x = 0, .y = 0, .z = 0}, .stepper_x = &motor_x, .stepper_y = &motor_y, .velocity = {.x = 0, .y = 0, .z = 0}}; /** * @brief 计算两个位置矢量之间的差 * * @param dest 终点坐标 * @param src 起点坐标 * @param res 计算结果 **/ static void v3_diff(struct vector3 *dest, struct vector3 *src, struct vector3 *res) { res->x = dest->x - src->x; res->y = dest->y - src->y; res->z = dest->z - src->z; } /** * @brief 实现矢量到矢量的复制 * @param dest 复制目的地址 * @param src 被复制地址 **/ static void v3_copy(struct vector3 *dest, struct vector3 *src) { dest->x = src->x; dest->y = src->y; dest->z = src->z; } /** * @brief 前往指定坐标 * * @param ins 运动控制器实例 * @param dest 目的坐标 **/ void motion_goto(struct motion_info *ins, struct vector3 *dest) { while (ins->motion_lock == 1) ; ins->motion_lock = 1; struct vector3 v3DisplaceXY; //XY坐标下的位移(mm) struct vector3 v3DisplaceMotor; //电机坐标系下的位移(mm) struct vector3 v3SpeedScaled; //放缩过后的速度矢量(step/s) v3_diff(dest, &ins->position, &v3DisplaceXY); v3_copy(&ins->position, dest); //根据目标结构进行坐标转换 #ifdef COREXY #endif v3DisplaceMotor.x = v3DisplaceXY.x + v3DisplaceXY.y; v3DisplaceMotor.y = v3DisplaceXY.x - v3DisplaceXY.y; #ifdef XY v3DisplaceMotor.x = v3DisplaceXY.x; v3DisplaceMotor.y = v3DisplaceXY.y; #endif //将恒定速度按照位移方向在X、Y分量上进行分解 int32_t vscale = STEP_PER_MM * sqrt(v3DisplaceMotor.x * v3DisplaceMotor.x + v3DisplaceMotor.y * v3DisplaceMotor.y); if (vscale == 0) { v3SpeedScaled.x = 0; v3SpeedScaled.y = 0; ins->motion_lock = 0; return; } else { // unit: mm/s -> steps/s v3SpeedScaled.x = v3DisplaceMotor.x * ins->max_speed * STEP_PER_MM * STEP_PER_MM / vscale; v3SpeedScaled.y = v3DisplaceMotor.y * ins->max_speed * STEP_PER_MM * STEP_PER_MM / vscale; } v3_copy(&ins->velocity, &v3SpeedScaled); //对电机资源加锁 ins->stepper_x->motor_lock = 1; ins->stepper_y->motor_lock = 1; //开启电机1 if (((int32_t)(v3SpeedScaled.x)) != 0) stepper_launch(ins->stepper_x, (int32_t)v3SpeedScaled.x, 1000000 * STEP_PER_MM * v3DisplaceMotor.x / v3SpeedScaled.x); else stepper_launch(ins->stepper_x, 0, 0); //开启电机2 if (((int32_t)(v3SpeedScaled.y)) != 0) stepper_launch(ins->stepper_y, (int32_t)v3SpeedScaled.y, 1000000 * STEP_PER_MM * v3DisplaceMotor.y / v3SpeedScaled.y); else stepper_launch(ins->stepper_y, 0, 0); //阻塞,直至所有电机空闲 while (ins->stepper_x->motor_lock == 1 || ins->stepper_y->motor_lock == 1) ; //释放运动控制器 ins->motion_lock = 0; } ``` ## 以太网控制指令的解析 接收部分负责接受来自上位机的信息,进行解析后根据控制数据对调用运动控制部分,启动运动。接收动作是阻塞的,且仅在已连接的状态下进行recv动作。解析部分与接收部分各开启一个线程。 以太网部分使用了Lwip+SAL+ETH协议栈,使用Socket通信方式即可轻松实现。 ```c /** @file network.c **/ #include "network.h" struct rt_ringbuffer rb; uint8_t rb_pool[512]; static int sock, connected, bytes_received; socklen_t sin_size; static struct sockaddr_in server_addr, client_addr; volatile rt_bool_t isConnected; uint8_t send_buf[256] = "This is TCP Server from RT-Thread."; uint16_t send_len; uint8_t rcv_cmd[32]; uint8_t rcv_len; /** * @brief 在5000端口上开启TCP服务端 * */ void server_init() { isConnected = 0; rt_ringbuffer_init(&rb, rb_pool, 512); //创建socket if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) { /* 创建失败的错误处理 */ rt_kprintf("Socket error\n"); return; } int32_t nonblock = 1; /* 初始化服务端地址 */ server_addr.sin_family = AF_INET; server_addr.sin_port = htons(5000); /* 服务端工作的端口 */ server_addr.sin_addr.s_addr = INADDR_ANY; rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero)); //将socket绑定到端口 if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1) { /* 绑定失败 */ rt_kprintf("Unable to bind\n"); return; } //监听 if (listen(sock, 5) == -1) { rt_kprintf("Listen error\n"); return; } rt_kprintf("\nTCPServer Waiting for client on port 5000...\n"); server_handle(); } /** * @brief 开始接收和解析动作,并不断在端口上进行应答 * */ void server_handle() { int32_t ret; rt_thread_t tRx, tTx, tCmd; //创建并开启线程 tRx = rt_thread_create("TCP RX", func_recv, RT_NULL, 2048, 17, 150); tCmd = rt_thread_create("RX CMD", rx_cmd, RT_NULL, 2048, 20, 150); rt_thread_startup(tRx); rt_thread_startup(tCmd); while (1) { sin_size = sizeof(struct sockaddr_in); connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size); // 连接失败 if (connected < 0) { rt_kprintf("accept connection failed! errno = %d\n", errno); continue; } rt_kprintf("I got a connection from (%s , %d)\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); isConnected = 1; // 至此,与客户端建立起了链接。以下是在该链接上进行的处理 ret = send(connected, send_buf, 34, 0); if (ret < 0) { /* 发送失败,关闭这个连接 */ closesocket(connected); isConnected = 0; rt_kprintf("\nsend error,close the socket.\r\n"); break; } if (ret == 0) rt_kprintf("\n Send warning,send function return 0.\r\n"); while (isConnected) ; } } /** * @brief 接收线程,将收到的数据存入Ringbuffer,以备解析线程处理 * */ void func_recv() { int32_t ret, bytes_received; uint8_t buf[256]; rt_memset(buf, 0, 256); while (1) { //若无连接,等待连接 if (isConnected == 0) { rt_thread_mdelay(100); continue; } bytes_received = recv(connected, buf, 256, 0); //异常处理 if (bytes_received < 0) { closesocket(connected); isConnected = 0; continue; } else if (bytes_received == 0) { rt_kprintf("\nReceived warning,recv function return 0.\r\n"); closesocket(connected); isConnected = 0; continue; } //接受数据并写入Ringbuffer rt_ringbuffer_put(&rb, buf, bytes_received); rt_memset(buf, 0, 256); } } /** * @brief 解析线程,从ringbuffer中读出控制命令,并控制运动部分 * */ void rx_cmd() { uint8_t buf; while (1) { while (RT_RINGBUFFER_EMPTY == rt_ringbuffer_data_len(&rb)) rt_thread_mdelay(10); rt_ringbuffer_get(&rb, rcv_cmd + rcv_len, 1); rcv_len++; if (rcv_cmd[rcv_len - 1] == '\n') { rcv_cmd[rcv_len] = 0; // 读取位置信息 struct vector3 dest; if (strstr(rcv_cmd, "X") != NULL && strstr(rcv_cmd, "Y") != NULL) { sscanf(rcv_cmd, "X%fY%f", &dest.x, &dest.y); motion_goto(&motion, &dest); rt_kprintf("the position of head is set to %.2f,%.2f\r\n", dest.x, dest.y); } rcv_len = 0; rt_memset(rcv_cmd, 0, 32); } } } //将函数注册到shell MSH_CMD_EXPORT(server_init, init the server); ``` # 实现效果 实现效果见视频:https://www.bilibili.com/video/BV1citieAEVt/ 项目地址:https://gitee.com/kurehatian/kureha-pnp # 总结 RT-Thread不仅是一个实时操作系统,更提供了丰富的组件包和高度抽象可移植的常用设备模型,面向多款主流开发板/芯片方案提供了BSP,大大缩短了物联网开发的时间和学习成本。使用线程来规划不同任务的执行,规避了裸机上开发一样要考虑任务间的切换和同步问题,有多种线程间通信手段来提高开发效率。
0
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
KurehaTian
这家伙很懒,什么也没写!
文章
1
回答
0
被采纳
0
关注TA
发私信
相关文章
1
步进电机驱动的packages
2
通过RTOS来控制电机应该怎么做呢?
3
如何在 RTT 的现有框架上对 PWM 的输出脉冲数进行计数
4
请教用RT-THREAD系统控制10个步进电机,用什么开发板?
5
rt-thread怎么使用高级定时器配置比较输出信号使用4通道
6
嵌入式实时操作系统的实时性能不能满足电机控制,多轴联动的需求?
7
stm32f103怎么使用捕获输出了多少脉冲的PWM
8
请教如何用高级定时器控制步进电机?
9
我用evk工具添加dhtxx的包显示如下错误
10
FRDM-MCXN947 DEBUG时候,突然烧录不进去了
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
ART-Pi
FinSH
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
rt-smart
FAL
ESP8266
I2C_IIC
WIZnet_W5500
ota在线升级
UART
cubemx
PWM
flash
packages_软件包
freemodbus
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
keil_MDK
rt_mq_消息队列_msg_queue
ulog
C++_cpp
at_device
本月问答贡献
出出啊
1516
个答案
342
次被采纳
小小李sunny
1440
个答案
289
次被采纳
张世争
799
个答案
171
次被采纳
crystal266
547
个答案
161
次被采纳
whj467467222
1222
个答案
148
次被采纳
本月文章贡献
出出啊
1
篇文章
1
次点赞
小小李sunny
1
篇文章
1
次点赞
张世争
1
篇文章
4
次点赞
crystal266
2
篇文章
2
次点赞
whj467467222
2
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部