通过网盘分享的文件:一个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:避免了 "参数改了一半就被发送出去" 的风险(比如位置刚改、速度还没改,就把不完整的指令发给电机),也防止了 cmd 与 ctrl 差值过大导致的电流冲击(如速度突变)。
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);