rtthread控制达妙4310电机

通过网盘分享的文件:一个4310电机位置控制.zip

链接: https://pan.baidu.com/s/1EBS39xcS-PMlrc07oezXfw?pwd=ck2t 提取码: ck2t

首先我们要建立最底层的通信系统:can通信

不会的,可以查看教程:

https://blog.csdn.net/qq_66669252/article/details/159949486?spm=1011.2415.3001.5331

步骤1:查找can1设备句柄,初始化信号量

步骤2:以中断收发模式打开设备,波特率设置1mbps,模式为正常模式

步骤3:绑定中断函数释放信号量,信号量释放后进行电机数据读取(发给电机数据,电机自动回传一帧数据)

can.c

cs 复制代码
#include "can.h"
#include <board.h>
#include "dm4310_drv.h"
#include "dm4310_ctrl.h"

#define CAN_DEV_NAME       "can1"
rt_device_t can_dev = RT_NULL;
static struct rt_semaphore rx_sem;

/* 接收中断回调:释放信号量 */
static rt_err_t can_rx_call(rt_device_t dev, rt_size_t size)
{
    rt_sem_release(&rx_sem);
    return RT_EOK;
}

/* CAN 接收处理线程 */
static void can_rx_thread_entry(void *parameter)//达妙电机自动返回数据,一问一答的形式
{
    struct rt_can_msg rxmsg = {0};
    while (1)
    {
        if (rt_sem_take(&rx_sem, RT_WAITING_FOREVER) == RT_EOK)
        {
            while (rt_device_read(can_dev, 0, &rxmsg, sizeof(rxmsg)) == sizeof(rxmsg))
            {
                /* 达妙电机默认返回的 CAN ID 通常是 MasterID(0x33)*/
                if (rxmsg.id == 0x33)
                {
                    /* rxmsg.data[0] 的低 4 位是回传的电机 ID */
                    if ((rxmsg.data[0] & 0x0F) == motor[Motor1].id)//0x33的masterid放在仲裁段,数据段里面放的是canid
                    {
                        // 丢给电机底层协议去解析具体的位置和速度
                        dm4310_fbdata(&motor[Motor1], rxmsg.data);
                    }
                }
            }
        }
    }
}

/* CAN 硬件初始化 (改为 NORMAL 模式) */
int can_init(void)
{
    rt_err_t res;
    rt_thread_t thread;

    can_dev = rt_device_find(CAN_DEV_NAME);
    if (!can_dev) return -RT_ERROR;

    rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);
    res = rt_device_open(can_dev, RT_DEVICE_FLAG_INT_RX | RT_DEVICE_FLAG_INT_TX);
    if (res != RT_EOK) return -RT_ERROR;

    /* 注意:达妙电机出厂波特率通常是 1Mbps。 CAN1MBaud */
    rt_device_control(can_dev, RT_CAN_CMD_SET_BAUD, (void *)CAN1MBaud);
    /* 必须使用正常模式与电机通信 */
    rt_device_control(can_dev, RT_CAN_CMD_SET_MODE, (void *)RT_CAN_MODE_NORMAL);
    rt_device_set_rx_indicate(can_dev, can_rx_call);

    thread = rt_thread_create("can_rx", can_rx_thread_entry, RT_NULL, 1024, 13, 10);
    if (thread) rt_thread_startup(thread);

    rt_kprintf("[CAN] Init Success (Normal Mode).\n");
    return RT_EOK;
}

搞定can通信之后,建立电机结构体完成数据存放与读取,将电机的硬件标识、运行状态、控制指令、反馈数据等所有信息封装在一起,起到了 "数据中枢" 的作用。

采用cmd与ctrl:避免了 "参数改了一半就被发送出去" 的风险(比如位置刚改、速度还没改,就把不完整的指令发给电机),也防止了 cmdctrl 差值过大导致的电流冲击(如速度突变)。

cs 复制代码
typedef struct {
    int id;//canid
    int state;//电机状态码,正常1,过流过热欠压会变成8、9等等
    int p_int, v_int, t_int, kp_int, kd_int;//16进制原码:位置,速度,扭矩。阻尼原码
    float pos, vel, tor, Kp, Kd, Tmos, Tcoil;//当前实际角度,当前实际速度,当前实际扭矩,驱动板子mos管温度,电机线圈温度
} motor_fbpara_t;

//想要电机做什么,就往这个结构体里写数据
typedef struct {
    int8_t mode;//0:mit模式。1:位置模式。2:速度模式
    float pos_set, vel_set, tor_set, kp_set, kd_set;//目标位置,目标速度,前馈扭矩,位置刚度,速度阻尼
} motor_ctrl_t;

typedef struct {
    int8_t id;//canid
    uint8_t start_flag;//0:电机断电。1:电机上电
    motor_fbpara_t para;//实际反馈状态
    motor_ctrl_t ctrl;//当前执行指令
    motor_ctrl_t cmd;//目标期望指令:防止结构体还没填完,数据把一半的结构体发出去了。防止cmd与ctrl差值过大,如:速度突然加大,造成电流急剧上升
} motor_t;

我们给电机发数据,最后一步使用就是rt_device_write()函数,我们就得考虑msg结构体如何填入数值,id号就是电机的canid,标准帧,数据帧,长度,然后8字节的数组。

cs 复制代码
static void rt_can_transmit(uint16_t id, uint8_t *data, uint8_t len)
{
    if (!can_dev) return;
    struct rt_can_msg txmsg = {0};
    txmsg.id = id;
    txmsg.ide = RT_CAN_STDID;//标准帧
    txmsg.rtr = RT_CAN_DTR;//数据帧
    txmsg.len = len;//data[8] 数组里有几个字节是有效的。
    for(int i = 0; i < len; i++) txmsg.data[i] = data[i];
    rt_device_write(can_dev, 0, &txmsg, sizeof(txmsg));
}

现在我们得考虑8字节数组填什么,我们如何让电机动起来。我们采用mit模式。

查看达妙说明书,我们需要把P,V,KP,KD,T的浮点数放入8字节数据中

float类型数据,一个是占4字节的。我们如何填入8字节数组

我们进行浮点数转int类型

cs 复制代码
int float_to_uint(float x_float, float x_min, float x_max, int bits) {
    float span = x_max - x_min;
    return (int) ((x_float - x_min) * ((float)((1 << bits) - 1)) / span);
}

转好之后,我们通过移位,分别放入8个格子中

cs 复制代码
void dm4310_ctrl_send(motor_t *motor)
{
    uint8_t data[8];
    uint16_t pos_tmp, vel_tmp, kp_tmp, kd_tmp, tor_tmp;//位置,速度,扭矩,kp,kd
//把浮点数实际控制值转换成16进制的整数
    pos_tmp = float_to_uint(motor->ctrl.pos_set, P_MIN, P_MAX, 16);//位置占16位,其余占12位,拼起来刚好8字节
    vel_tmp = float_to_uint(motor->ctrl.vel_set, V_MIN, V_MAX, 12);
    kp_tmp  = float_to_uint(motor->ctrl.kp_set,  KP_MIN, KP_MAX, 12);
    kd_tmp  = float_to_uint(motor->ctrl.kd_set,  KD_MIN, KD_MAX, 12);
    tor_tmp = float_to_uint(motor->ctrl.tor_set, T_MIN, T_MAX, 12);
//数据拼装,can通信数据段是8字节,
    data[0] = (pos_tmp >> 8);
    data[1] = pos_tmp;
    data[2] = (vel_tmp >> 4);
    data[3] = ((vel_tmp & 0xF) << 4) | (kp_tmp >> 8);
    data[4] = kp_tmp;
    data[5] = (kd_tmp >> 4);
    data[6] = ((kd_tmp & 0xF) << 4) | (tor_tmp >> 8);
    data[7] = tor_tmp;

    rt_can_transmit(motor->id + MIT_MODE, data, 8);
}

这时我们需要考虑ctrl.pos_set等数据如何得到

我们需要把cmd的数据统一给ctrl结构体保存,这样才不会出现("参数改了一半就被发送出去" )

cs 复制代码
void dm4310_set(motor_t *motor)
{
    motor->ctrl.pos_set = motor->cmd.pos_set;//目标期望值拷贝到实际控制值中:数据全部准备好了,一起拷入
    motor->ctrl.vel_set = motor->cmd.vel_set;
    motor->ctrl.kp_set  = motor->cmd.kp_set;
    motor->ctrl.kd_set  = motor->cmd.kd_set;
    motor->ctrl.tor_set = motor->cmd.tor_set;
}

cmd结构体的数据又如何得到?

我们通过电机初始化来得到

cs 复制代码
void dm4310_motor_init(void)
{
    memset(&motor[Motor1], 0, sizeof(motor[Motor1]));//内存清零函数,填入结构体数组0位置元素地址,根据motor数组的大小,全部变成0

    /* 根据你的需求配置:CAN ID=0x03, MIT模式(0) */
    motor[Motor1].id = 0x03;
    motor[Motor1].ctrl.mode = 0;//当前实际发送给底层驱动的模式也是mit模式
    motor[Motor1].cmd.mode  = 0;//我的期望是让他工作在mit模式
    motor[Motor1].cmd.tor_set = 0.12f;

    /* 给一点微小的阻尼参数,防止 MIT 模式下电机失控狂奔 */
    motor[Motor1].cmd.kp_set = 0.0f; // 暂不给位置刚度
    motor[Motor1].cmd.kd_set = 0.5f; // 给一点速度阻尼
}

整体启动逻辑:

我们在终端输入motor_start,调用线程motor_ctrl_thread_entry,线程进行了电机参数初始化dm4310_motor_init();使能电机ctrl_enable();

每秒500次:在循环里面不断的把cmd目标值给ctrl实际控制值。

cs 复制代码
static void motor_ctrl_thread_entry(void *parameter)
{
    /* 1. 初始化底层的 CAN 硬件 */
    if (can_init() != RT_EOK)
    {
        rt_kprintf("[ERR] CAN Init Failed. Thread Exit.\n");
        return;
    }

    /* 2. 初始化电机参数 */
    dm4310_motor_init();
    rt_thread_mdelay(100);

    /* 3. 启用电机 */
    ctrl_enable();
    rt_kprintf("[MOTOR] Start control loop (%d ms)\n", MOTOR_CTRL_PERIOD_MS);

    /* 4. 2ms 周期实时控制循环 */
    while (1)
    {
        // 此处可添加控制算法,修改 motor[Motor1].cmd 的参数

        ctrl_set();   // 同步指令到控制结构体
        ctrl_send();  // 将数据打入 CAN 发送队列

        // 打印实时回传位置 (频率极高,调试完建议注释掉)
        // rt_kprintf("Pos: %.2f\n", motor[Motor1].para.pos);

        // 精准阻塞延时
        rt_thread_mdelay(MOTOR_CTRL_PERIOD_MS);
    }
}

/* 导出终端命令 */
int motor_start(void)
{
    rt_thread_t thread = rt_thread_create("mot_app", motor_ctrl_thread_entry, RT_NULL, 2048, 12, 10);
    if (thread != RT_NULL) {
        rt_thread_startup(thread);
        return RT_EOK;
    }
    return -RT_ERROR;
}
MSH_CMD_EXPORT(motor_start, Start DM4310 motor control loop);

我们还能在终端里设置控制函数:让电机运动到特定位置

cs 复制代码
/* ================================================================= *
 * 调试命令 1:设置电机目标位置
 * 用法:在终端输入 motor_set_pos <位置值>
 * 例如:motor_set_pos 5.0  (让电机转到 5.0 弧度)
 * ================================================================= */
static void motor_set_pos(int argc, char **argv)
{
    if (argc < 2)
    {
        rt_kprintf("Usage: motor_set_pos <position>\n");
        rt_kprintf("Example: motor_set_pos 3.14\n");
        return;
    }

    // 将输入的字符串转换为浮点数
    float target_pos = atof(argv[1]);

    // 为了安全起见,限制一下输入范围 (防飞车)
    if (target_pos > P_MAX || target_pos < P_MIN)
    {
        rt_kprintf("[Warning] Position out of range! (%.1f to %.1f)\n", P_MIN, P_MAX);
        return;
    }

    // 更新命令结构体里的位置目标,并给予一定的刚度和阻尼
    motor[Motor1].cmd.pos_set = target_pos;
    motor[Motor1].cmd.kp_set = 3.0f; // 给予位置刚度 (数值越大,响应越猛,请慢慢调大)
    motor[Motor1].cmd.kd_set = 0.5f;  // 给予速度阻尼 (防止震荡)

    rt_kprintf("[CMD] Set Motor1 Target Position to: %.2f\n", target_pos);
}
MSH_CMD_EXPORT(motor_set_pos, Set DM4310 target position);

只要给电机发送数据,电机自动回传一帧数据

我们通过can的接收线程,有数据进入stm32,自动调用接收中断,释放信号量,调用 dm4310_fbdata(&motor[Motor1], rxmsg.data);进行整形转换成float值

cs 复制代码
static void can_rx_thread_entry(void *parameter)//达妙电机自动返回数据,一问一答的形式
{
    struct rt_can_msg rxmsg = {0};
    while (1)
    {
        if (rt_sem_take(&rx_sem, RT_WAITING_FOREVER) == RT_EOK)
        {
            while (rt_device_read(can_dev, 0, &rxmsg, sizeof(rxmsg)) == sizeof(rxmsg))
            {
                /* 达妙电机默认返回的 CAN ID 通常是 MasterID(0x33)*/
                if (rxmsg.id == 0x33)
                {
                    /* rxmsg.data[0] 的低 4 位是回传的电机 ID */
                    if ((rxmsg.data[0] & 0x0F) == motor[Motor1].id)//0x33的masterid放在仲裁段,数据段里面放的是canid
                    {
                        // 丢给电机底层协议去解析具体的位置和速度
                        dm4310_fbdata(&motor[Motor1], rxmsg.data);
                    }
                }
            }
        }
    }
}

我们把传回来的数据保存在para结构体中,我们就可以在终端进行读取。

还需要对数据进行解算:

cs 复制代码
float uint_to_float(int x_int, float x_min, float x_max, int bits) {
    float span = x_max - x_min;
    return ((float)x_int) * span / ((float)((1 << bits) - 1)) + x_min;
}
cs 复制代码
//电机整型数据转换为结构体的值
void dm4310_fbdata(motor_t *motor, uint8_t *rx_data)
{
    motor->para.id = (rx_data[0]) & 0x0F;//第0个字节同时装了两个 4 位的数据:高 4 位是电机状态(State),低 4 位是电机 ID。
    motor->para.state = (rx_data[0]) >> 4;
    motor->para.p_int = (rx_data[1] << 8) | rx_data[2];
    motor->para.v_int = (rx_data[3] << 4) | (rx_data[4] >> 4);
    motor->para.t_int = ((rx_data[4] & 0xF) << 8) | rx_data[5];
    motor->para.Tmos  = (float)((int8_t)rx_data[6]);
    motor->para.Tcoil = (float)((int8_t)rx_data[7]);

    motor->para.pos = uint_to_float(motor->para.p_int, P_MIN, P_MAX, 16);
    motor->para.vel = uint_to_float(motor->para.v_int, V_MIN, V_MAX, 12);
    motor->para.tor = uint_to_float(motor->para.t_int, T_MIN, T_MAX, 12);
}

在终端输入命令:查看电机当前实时数据

cs 复制代码
/* ================================================================= *
 * 调试命令 2:查看电机当前实时状态
 * 用法:在终端输入 motor_info
 * ================================================================= */
static void motor_info(int argc, char **argv)
{
    rt_kprintf("====== DM4310 Motor Status ======\n");
    rt_kprintf(" ID    : 0x%02X\n", motor[Motor1].para.id);
    rt_kprintf(" State : %d\n", motor[Motor1].para.state);
    rt_kprintf(" Pos   : %.3f rad\n", motor[Motor1].para.pos);
    rt_kprintf(" Vel   : %.3f rad/s\n", motor[Motor1].para.vel);
    rt_kprintf(" Torq  : %.3f Nm\n", motor[Motor1].para.tor);
    rt_kprintf(" T_mos : %.1f C\n", motor[Motor1].para.Tmos);
    rt_kprintf(" T_coil: %.1f C\n", motor[Motor1].para.Tcoil);
    rt_kprintf("=================================\n");
}
MSH_CMD_EXPORT(motor_info, Print DM4310 real-time status);
相关推荐
吕源林1 天前
如何解决SQL存储过程连接泄露_确保在异常后关闭连接
jvm·数据库·python
andylauren1 天前
论单点接地的重要性——从MP3模块噪声问题看接地设计的关键
嵌入式硬件
Gofarlic_OMS1 天前
应对MathWorks合规审查的专项准备工作
大数据·服务器·网络·数据库·人工智能
七夜zippoe1 天前
DolphinDB SQL查询:从基础到进阶
数据库·sql·进阶·聚合·dolphindb
修勾勾L1 天前
使用VSCode开发嵌入式开发详细教程——步骤二项目实战
嵌入式硬件
有想法的py工程师1 天前
PostgreSQL 深入heap_update() 与 HOT 机制(附源码级解析)
数据库·postgresql
染予1 天前
定时器时钟源介绍
单片机·嵌入式硬件
时空自由民.1 天前
ESP32编译固件内存信息解读
单片机·性能优化
qq_342295821 天前
如何为容器内多个列表实现统一滚动条.txt
jvm·数据库·python