基于RT-Thread的CAN电机驱动板设计 (二)uart串口DMA的实现

引言

在上一篇文章中完成了对项目的需求分析以及硬件电路的设计和制作。接下来就开始项目软件方面的实现。

  • 首先本章完成UART实现485通信的配置和调试,同时UART采用DMA方式传输,DMA控制器代替CPU完成数据传输工作,腾出时间给CPU,提高了CPU的使用的使用效率。完成PC上位机和电机驱动板的通讯。
  • 然后在下一章完成rt-thread的CAN驱动的通讯与配置,完成CAN电机和电机驱动板的通讯。
  • 最后实现按键中断,在中断中通过CAN发送电机启停信号完成电机启停控制

资料连接

本项目的所有资料全部开源:

硬件工程https://lceda.cn/FranHawk/485tocan_motor_controller
软件工程https://github.com/FranHawk/RT-Thread-485toCAN

程序框架

前期准备

  • 制作好的电机驱动板一块
  • USB转485模块一个,用来插在PC上实现485协议与电机驱动板通讯
  • rtthread studio开发环境
  • cubemx配置工具
    硬件连接图如下所示,蓝色的就是USB转485模块

    开发过程

    创建RT-Thread工程

    打开rtthread studio,根据芯片型号新建工程,根据提示,需要将时钟源改为外部晶振


打开cubemx,根据芯片型号创建cubemx工程,高速时钟源选择外部晶振

并按照如下图配置时钟树

最后通过这个cubemx工程生成一个MDK5的工程,打开这个MDK5工程

在main.c中找到时钟配置的代码如下,并且复制到rtthread studio的drv_clk.c文件的system_clock_config函数中去,覆盖掉原内容

复制后的system_clock_config函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void system_clock_config(int target_freq_Mhz)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}

至此rtthread studio工程创建完毕,并完成外部时钟源配置

配置带DMA功能的UART

使用板子上的485A接口,根据电路图有



485A芯片用的是UART3,同时由于485是一种半双工的协议,还有RE引脚控制收发,RE为低电平时为接收,高电平时为发送。
打开rtthread settings配置串口


使能串口DMA模式,保存并关闭rtthread settings,打开board.h,根据注释的提示,输入UART3的引脚信息

打开stm32f1xx_hal_conf.h,注释掉DMA的使能如下图

保存并编译。

指令定义

根据实际需求,我们需要向CAN电机发送转矩电流指令和查询转矩电流和电机编码器位置的指令。我们要做的不是简单地将上位机的指令直接由485协议换成CAN协议转发到电机,而是通过自定义一套指令,对指令进行拆分和解码,然后再向电机发送相应的控制指令。
首先查看电机的数据手册。

转矩电流控制指令


电机状态查询指令


发现他们的消息回复是一样的,因此收到两种消息我们可以按照同一种方式解码

上位机向驱动板指令定义

根据上面的指令,我们自定义了一套上位机向驱动板的指令
首先是转矩电流控制指令

然后是状态查询指令

最后电机驱动板收到电机上报的状态后,也需要通过解码最后拼接成一条指令传到上位机

在博客中可能不太清楚,这个定义指令的文件我都会开源出来

参考rtthread官网的UART设备的示例程序编写代码

https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/uart/uart

首先定义引脚名字和设备名称

1
2
3
/* RS485A串口名称和收发控制引脚定义*/
#define RS485A_UART_NAME "uart3"
#define RS485A_RE_PIN GET_PIN(B,1)

串口设置为DMA中断接收方式,DMA接收完成串口数据后调用完成接收中断接收中断通过一个消息队列,向串口数据处理线程发送设备句柄和消息长度。串口数据处理线程接收到消息队列传过来的数据后完成后续的数据处理工作。这里借鉴了中断要快进快出的思想,接收中断中仅向消息队列发送消息,然后退出,串口数据处理线程完成剩余的数据处理任务,相当于是中断的底半部分。参考链接如下:
https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/interrupt/interrupt
消息队列的消息定义如下

1
2
3
4
5
6
/* 串口接收消息结构*/
struct rs485_serial_rx_msg
{
rt_device_t dev;
rt_size_t size;
};

串口设备句柄,消息队列控制块,串口配置结构体定义如下

1
2
3
4
5
6
/* 串口设备句柄 */
static rt_device_t rs485_serial_device_handle;
/* 消息队列控制块 */
static struct rt_messagequeue rs485_serial_rx_mq;
/* 串口配置结构体 */
struct serial_configure rs485_serial_config = RT_SERIAL_CONFIG_DEFAULT;

485串口初始化配置以及数据处理线程初始化,波特率115200,8位数据位1位停止位,优先级设为6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/* rs485串口接收数据线程初始化 */
static rt_err_t uart_dma_thread_init()
{
rt_err_t ret = RT_EOK;
static char msg_pool[256];

/* 初始化消息队列 */
rt_mq_init(&rs485_serial_rx_mq, "rx_mq", msg_pool, /* 存放消息的缓冲区 */
sizeof(struct rs485_serial_rx_msg), /* 一条消息的最大长度 */
sizeof(msg_pool), /* 存放消息的缓冲区大小 */
RT_IPC_FLAG_FIFO); /* 如果有多个线程等待,按照先来先得到的方法分配消息 */

/* 收发控制引脚使能并拉低 */
rt_pin_mode(RS485A_RE_PIN, PIN_MODE_OUTPUT);
rt_pin_write(RS485A_RE_PIN, PIN_LOW);

/* 查找串口设备 */
rs485_serial_device_handle = rt_device_find(RS485A_UART_NAME);
if (!rs485_serial_device_handle)
{
rt_kprintf("find %s failed!\n", RS485A_UART_NAME);
return RT_ERROR;
}

/* 修改串口配置参数 */
rs485_serial_config.baud_rate = BAUD_RATE_115200; //修改波特率为 115200
rs485_serial_config.data_bits = DATA_BITS_8; //数据位 8
rs485_serial_config.stop_bits = STOP_BITS_1; //停止位 1
rs485_serial_config.bufsz = 128; //修改缓冲区 buff size 为 128
rs485_serial_config.parity = PARITY_NONE; //无奇偶校验位

/* 控制串口设备。通过控制接口传入命令控制字,与控制参数 */
rt_device_control(rs485_serial_device_handle, RT_DEVICE_CTRL_CONFIG, &rs485_serial_config);

/* 以 DMA接收及轮询发送方式打开串口设备 */
rt_device_open(rs485_serial_device_handle, RT_DEVICE_FLAG_DMA_RX);

/* 设置接收回调函数 */
rt_device_set_rx_indicate(rs485_serial_device_handle, uart_input);

/* 创建 serial 线程,优先级6 */
rt_thread_t serial_thread_handle = rt_thread_create("rs485_serial", rs485_serial_thread_entry, RT_NULL, 2048, 6,
10);
/* 创建成功则启动线程 */
if (serial_thread_handle != RT_NULL)
{
rt_thread_startup(serial_thread_handle);
}
else
{
ret = RT_ERROR;
}

return ret;
}

下面这个函数是DMA接收完成中断服务函数,本着快进快出的原则,中断函数仅将串口设备句柄和接收数据的长度发送到消息队列,数据处理线程从消息队列获取信息,完成剩下的数据处理工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 串口接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
struct rs485_serial_rx_msg msg;
rt_err_t result;
msg.dev = dev;
msg.size = size;

/* 向消息队列发送消息 */
result = rt_mq_send(&rs485_serial_rx_mq, &msg, sizeof(msg));
if (result == -RT_EFULL)
{
/* 消息队列满 */
rt_kprintf("message queue full!\n");
}
return result;
}

数据处理线程根据上面定义的指令格式,对上位机发送过来的指令进行解码,首先判断帧头帧尾是否正确,然后判断指令类型,完成校验和的计算,最后分别向8个电机通过CAN发送相应格式的指令,最后将电机上报的状态向上位机发送。数据处理线程涉及到一些CAN的部分,这部分下一篇文章会讲,在本文中可以先忽略,重点在于接收并处理串口DMA接收的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
/* 串口处理接收数据线程入口 */
static void rs485_serial_thread_entry(void *parameter)
{
rt_size_t size;
struct rs485_serial_rx_msg msg;
struct rt_can_msg can_tx_msg = { 0 };
rt_err_t result;
rt_uint32_t rx_length;
rt_uint8_t check_sum, check_index;
static char rx_buffer[RT_SERIAL_RB_BUFSZ + 1];

while (1)
{
rt_memset(&msg, 0, sizeof(msg));
/* 从消息队列中读取消息*/
result = rt_mq_recv(&rs485_serial_rx_mq, &msg, sizeof(msg), RT_WAITING_FOREVER);
if (result == RT_EOK)
{
//rt_kprintf("%d\n", msg.size);
/* 从串口读取数据*/

rx_length = rt_device_read(msg.dev, 0, rx_buffer, msg.size);

if ((rx_buffer[0] == 0x5A) && (rx_buffer[18] == 0xA5))
{
//rt_kprintf("recevice success\n");
//判断指令类型,若不为0xFF,则是转矩控制指令,否则为状态查询指令
if ((rx_buffer[1] != 0xFF) && (rx_buffer[17] != 0xFF))
{
check_sum = 0;
for (check_index = 1; check_index < 17; check_index++)
{
check_sum += rx_buffer[check_index];
}
if ((check_sum != rx_buffer[17]) && (rx_buffer[17] != 0x00))
{
rt_kprintf("check_sum wrong\n");
rt_kprintf("%02x\n", check_sum);
}
else
{
//rt_kprintf("check_sum right\n");

/* 向电机发送转矩电流控制数据 */
can_tx_msg.ide = RT_CAN_STDID; /* 标准格式 */
can_tx_msg.rtr = RT_CAN_DTR; /* 数据帧 */
can_tx_msg.len = 8; /* 数据长度为 8 */
can_tx_msg.data[0] = TORQUE_CURRENT_CMD;
can_tx_msg.data[1] = 0x00;
can_tx_msg.data[2] = 0x00;
can_tx_msg.data[3] = 0x00;
can_tx_msg.data[6] = 0x00;
can_tx_msg.data[7] = 0x00;

/* CAN向电机1发送数据 */
can_tx_msg.id = MOTOR1_ID;
can_tx_msg.data[4] = rx_buffer[1];
can_tx_msg.data[5] = rx_buffer[2];
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机2发送数据 */
can_tx_msg.id = MOTOR2_ID;
can_tx_msg.data[4] = rx_buffer[3];
can_tx_msg.data[5] = rx_buffer[4];
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机3发送数据 */
can_tx_msg.id = MOTOR3_ID;
can_tx_msg.data[4] = rx_buffer[5];
can_tx_msg.data[5] = rx_buffer[6];
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机4发送数据 */
can_tx_msg.id = MOTOR4_ID;
can_tx_msg.data[4] = rx_buffer[7];
can_tx_msg.data[5] = rx_buffer[8];
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机5发送数据 */
can_tx_msg.id = MOTOR5_ID;
can_tx_msg.data[4] = rx_buffer[9];
can_tx_msg.data[5] = rx_buffer[10];
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机6发送数据 */
can_tx_msg.id = MOTOR6_ID;
can_tx_msg.data[4] = rx_buffer[11];
can_tx_msg.data[5] = rx_buffer[12];
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机7发送数据 */
can_tx_msg.id = MOTOR7_ID;
can_tx_msg.data[4] = rx_buffer[13];
can_tx_msg.data[5] = rx_buffer[14];
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机8发送数据 */
can_tx_msg.id = MOTOR8_ID;
can_tx_msg.data[4] = rx_buffer[15];
can_tx_msg.data[5] = rx_buffer[16];
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));
for (check_index = 1; check_index < 33; check_index++)
{
serial_tx_buffer[33] += serial_tx_buffer[check_index];
}
rt_pin_write(RS485A_RE_PIN, PIN_HIGH);/*使485处于发送模式*/
rt_device_write(rs485_serial_device_handle, 0, serial_tx_buffer, sizeof(serial_tx_buffer));
rt_pin_write(RS485A_RE_PIN, PIN_LOW);/*使485处于接收模式*/
}
}
else
{
/* 向电机发送状态查询指令 */
can_tx_msg.ide = RT_CAN_STDID; /* 标准格式 */
can_tx_msg.rtr = RT_CAN_DTR; /* 数据帧 */
can_tx_msg.len = 8; /* 数据长度为 8 */
can_tx_msg.data[0] = STATE_QUEST_CMD;/* 指令类型为状态查询 */
can_tx_msg.data[1] = 0x00;
can_tx_msg.data[2] = 0x00;
can_tx_msg.data[3] = 0x00;
can_tx_msg.data[4] = 0x00;
can_tx_msg.data[5] = 0x00;
can_tx_msg.data[6] = 0x00;
can_tx_msg.data[7] = 0x00;

/* CAN向电机1发送数据 */
can_tx_msg.id = MOTOR1_ID;
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机2发送数据 */
can_tx_msg.id = MOTOR2_ID;
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机3发送数据 */
can_tx_msg.id = MOTOR3_ID;
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机4发送数据 */
can_tx_msg.id = MOTOR4_ID;
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机5发送数据 */
can_tx_msg.id = MOTOR5_ID;
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机6发送数据 */
can_tx_msg.id = MOTOR6_ID;
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机7发送数据 */
can_tx_msg.id = MOTOR7_ID;
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

/* CAN向电机8发送数据 */
can_tx_msg.id = MOTOR8_ID;
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &can_tx_msg, sizeof(can_tx_msg));

//通过485向上位机发送数据
for (check_index = 1; check_index < 33; check_index++)
{
serial_tx_buffer[33] += serial_tx_buffer[check_index];
}
rt_pin_write(RS485A_RE_PIN, PIN_HIGH);/*使485处于发送模式*/
rt_device_write(rs485_serial_device_handle, 0, serial_tx_buffer, sizeof(serial_tx_buffer));
rt_pin_write(RS485A_RE_PIN, PIN_LOW);/*使485处于接收模式*/
}
}
}
}
}

BUG解决(很重要)

在串口DMA接收的过程中发现一个现象,发送一个固定长度的指令,DMA中断会遇到拆包的问题,举例如下:
上位机发送数据:
0x5A 0x4C 0x04 0xB0 0x04 0x14 0x05 0x78 0x05 0xDC 0x05 0x40 0x06 0xA4 0x06 0x08 0x07 0x7A 0xA5
DMA会用两个中断接收数据,指令就被拆包了不能正确的译码:
第一次中断接收数据:0x5A 0x4C 0x04 0xB0 0x04 0x14 0x05 0x78 0x05 0xDC 0x05 0x40 0x06
第二次中断接收数据:0xA4 0x06 0x08 0x07 0x7A 0xA5

一直没能解决这个问题,最后在论坛中发现了答案,链接:
https://club.rt-thread.org/ask/article/307.html
https://club.rt-thread.org/ask/question/427511.html
在drv_usart.c中,找到以下两个函数,分别是DMA接收完成和半完成函数,将dma_isr函数注释掉编译即可

总结

本文完成了基于rtthread的uart应用代码的编写,实现了电机驱动板和上位机通过485协议的通讯,主要是参照了rtthread官网的文档。下一篇文章实现电机驱动板和CAN电机通过CAN协议通讯,主要完成基于rtthread的CAN驱动的配置。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2021 张竞豪的小岛 All Rights Reserved.

UV : | PV :