4 Z2 R" u$ F4 d* [! b- f
0 ~5 z" X, X. ]2 f/ V. |以简单的智能车为例,一般会存在两个控制器,一个是运行ROS的主控,另一个是运行电机控制和传感器信息采集的单片机比如STM32。 . p# u; I8 H$ N7 a, o
' I( m$ I8 J/ Y4 g0 o, X% u/ A6 e5 N. y6 r# b- s7 E& o+ f3 w
由于存在多个控制器,完成一个机器人的具体任务,那么这多个控制器间则需要建立通信,本篇博客主要讲解: * E7 e" P$ S! F) k$ r- T
如何实现ROS主控和STM32之间的通信 ROS主控对STM32发送过来的数据做哪些处理
" Z0 M7 e# B9 u5 D7 k. n z
/ H* P5 ?: W" F: n
以智能车的应用例程展开
0 S5 C" O! d& |( n j. R# l
/ m" [: e& v8 l2 u! @
智能车控制器功能 在智能车里存在两个控制器: ROS主控 STM32控制器 9 K$ t: b7 v/ P, ?
# a' L& g7 R- o$ k
只要能将ROS跑起来就可以作为ROS主控,ROS主控可以是: jetson 系列,例如 nano、tx、nx 树莓派 工控机 6 {$ b! a& _4 k- I) O% x# B) L
4 e" Z! H* N b6 N( a
在智能车里,ROS主控主要实现的功能有: 雷达信息采集 摄像头信息采集 路径规划 定位 , Q) K, k k4 A- U b) F: r
: c5 X0 \' O4 @$ E" x+ G( D
STM32 控制器主要实现的功能有: 里程计信息采集 陀螺仪信息采集 电机控制 . _$ h1 Q$ H: D. [4 S" l# R8 }& O
$ Q( Y* ?, v( I2 x0 ]/ `; `
通信内容 6 k8 E5 m% }- Z+ ]3 P
5 g1 I$ j% J* N" e8 Z8 @8 S
" N. a s0 O9 z$ D
ROS主控负责接收stm32发送过来的传感器数据
9 C3 G5 ~8 Y& n$ n
数据有里程计、imu、电池电压。其中里程计就是电机的转速,通过编码器采集到。 2 D# P4 X# H. |4 Y) `/ r6 l
STM32负责接收ROS主控发送过来的运动底盘的目标速度,STM32再完成电机转速的控制,最终实现小车的移动任务
) c0 ]8 f& K8 b( q; i/ W$ N, u9 j8 E
ROS主控与STM32之间需要做到一个双向的数据传输,这里就涉及到了两个控制器之间的通信问题,下面则介绍如何实现两者之间的通信
/ J) }2 j0 V+ w2 M) b' d; h/ s
$ R. k; E' j2 q
硬件连接 * q0 p* h' ?, @1 y; ~
v3 H Y$ [# l' K
& B, B9 Y# z+ b& Q0 F j' l
ROS主控通过usb线连接到一个TTL电平转换芯片,再由这个电平转换芯片连接STM32芯片 ; u9 A2 o; B4 f' W0 J" j
电平转换芯片可以通过PCB设计在STM32芯片的电路板上,也可以使用一个USB转TTL的模块。
2 I }7 D! c4 T# f+ m+ S3 n2 X) u) A& n1 J# Q
# S1 }: t0 g1 O- J1 c
, M2 ?$ z# ?( L) B) l# J8 r- p6 V/ o/ ]/ @3 u# W5 r
为什么两个控制器之间需要电平转换芯片? , F' Z# \/ q. ~ {8 h
因为两个控制器之间通信层次逻辑是不同的,所有需要电平转换芯片。相当于两个主控是两种不同语言的人,电平转换芯片相当于一个翻译。 ! o" q, T' J% d, W
电平转换芯片可以是: cp2102 ch340 PL2303 FT232RL 8 j( g/ r- Q0 }9 C+ }3 C4 l6 Z* D
4 x1 Y' K7 ^5 u4 }" O1 T& R
# y0 }* v' \5 F# h
软件设置 硬件连接上之后,需要一个软件设置
; L" X3 D( {" P/ ~% T8 v8 r( O
需要软件设置原因: ROS主控可能接入多个USB设备,或者接入两个型号一样的电平转换芯片。 , D) x- Y5 ^! m! N+ j, b
不同USB设备占用的ROS主控的端口号在每次上电时可能会不一致,这样需要手动修改代码中的配置参数,比较麻烦,也无法做到自启动。 1 Z' z* b# h# |& [9 l7 N
如果存在多个USB设备,但是每种USB设备的电平转换芯片不一样,那么我们可以根据芯片名称来知道端口 号,但是如果有两个芯片一样的电平转换芯片,则无法区分,这时候想做自启动那么必须要进行下面步骤的软件设置
$ E: |5 V' @- p1 U7 \0 ? ~
软件设置分为两步: 第一步是更改电平转换芯片的serial, 第二步是创建设备别名 8 l: O8 C4 B3 {$ \
- n5 P0 V! E* h0 Z5 T
更新电平转换芯片的serial
( m# ]) x* @& ^$ O. |2 B
首先在win环境下安装更改芯片serial的软件 & N3 E! @! r# \
CP21xx Customization Utility.exe 4 Y Z) z7 g: a- x
这个软件在网上下载就可以 0 y. U2 ^/ U' n0 a* a
打开这个软件,然后将芯片连接电脑的USB
$ g5 ^4 N; T: Z; C
) R6 [9 |8 |+ T& w$ I! c1 }0 p% k. P. ?) i/ M" j
然后将圆圈位置改为0002 $ K" E- C; A R$ M& o* \- U+ b
然后点击Program Device , s% S- d$ y, E. z2 @
- s8 c* o; k' d6 @
9 d" v1 `9 Q" B5 j c
点完之后要等下,在Status Logging窗口中出现下面信息,才说明修改好了 - z: {/ J& F3 F2 o
- U. O; ?! l E D
3 F3 G4 J: V5 \* \4 B( }
创建设备别名 : R+ o: M, ]! q7 Y; P. ]
需要创建设备别名原因:
- m, ?% e! o- [1 N0 \' ^$ {6 J在运行一个ros程序的时候需要提供一个端口名,这个端口名一般是ttyUSBx,设备每次插拔对应的这个端口名它都会不一样,需要创建一个设备别名,就是要将这个端口名来给它固定住。
: y c) D& T* ]5 V; ^9 e
6 t/ |* i- O. j3 q, L( s4 H
( f- N) P- e' M
重新插拔 USB1端口的设备后,变为: , N: ~7 A- Q+ F, `4 \
4 p6 k/ X# D8 w& o6 }* F; m2 `0 m$ y
4 F- J: V8 Z* b( G) u/ g2 B1 L
可以看到变成了/dev/ttyUSB2 / w* J, |, G7 ^: j$ C% b8 Q
端口号发生了变化
' [8 `% b- Q [5 N
创建设备别名需要写一个脚本文件,如下: - echo 'KERNEL=="ttyUSB*", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60",ATTRS{serial}=="0002", MODE:="0777", GROUP:="dialout", SYMLINK+="stm32_controller"' >/etc/udev/rules.d/stm32_controller.rules
; S% v# w7 a3 p% L- v0 x - echo 'KERNEL=="ttyUSB*", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60",ATTRS{serial}=="0001", MODE:="0777", GROUP:="dialout", SYMLINK+="2d_lidar"' >/etc/udev/rules.d/2d_lidar.rules
7 v ~5 f; ^/ M1 Q4 I* y
$ l1 B o I5 ^ ?7 Q% _- r- service udev reload
$ j9 Y# n9 D) _0 `4 h: E( \5 @ - sleep 2
3 n7 r& u$ B. J% e - service udev restart
复制代码6 R& K# I6 J5 {1 m A1 E
解释下上面的代码
9 ?7 T7 m7 g' \( C) V2 SKERNEL==”ttyUSB* 不管是USB几的设备都进行判断
9 h2 a, B, _2 o; T. ~! G) M: k/ \ATTRS{idVendor}==”10c4” 这里的idVendor ,在前面win上修改 serial的时候出现过,就是10c4
5 v1 p5 u8 D$ E! T6 b
2 |$ a: J# f, ~, O* w0 X
+ @! U+ R3 }( W- j
ATTRS{idProduct}==”ea60” 这里的idProduct,在前面win上修改 serial的时候出现过,就是ea60 ) u( L' Y$ H$ ~9 t. I
. \% _$ ^0 q0 [( H
( M9 Y5 M& R& t& m C3 Y, ?9 X
ATTRS{serial}==”0002” 这里的serial,就是前面我们改过的,这里就根据这个值的不同,定义不同的设备别名
p# ^4 C' f. ~: \+ fMODE:=”0777” 就是端口的权限
; s3 t* `! y) @
SYMLINK+=”stm32_controller” 这里的stm32_controller,就是取的设备别名。 * U8 e( ~* ]$ Z9 s% L
所有上面第一行代码的功能就是,将满足这些条件的端口的设备名称改为定义的设备别名。
- y4 s4 g4 {4 q. \
脚本的文件名称,取名为change_udev.sh。在执行前需要给这个脚本文件赋予权限。 - sudo chmod 777 change_udev.sh
复制代码# g0 q: H9 w1 S5 ^& _
赋予权限后,再运行这个脚本
$ J/ K3 p$ L: C$ t* q. E
这样就运行了设备别名的设置,之后不管怎么插拔这两个USB设备,系统都将会自动的将这两个设备去给它赋予设置的设备别名。 4 M2 x& Z2 ^- g& B! a% D7 A( p
使用设备别名
1 N; l4 \: k/ E) @8 I( E
在上面,设置了设备别名,下面来看如何使用我们的设备别名。 $ n$ |: s2 J; U6 i) }9 n; V
例如我们将雷达的USB的设备别名改为了2d_lidar
( W Y& Q% N6 [+ Z V- d7 o
雷达的roslaunch启动文件则可以写成如下: : k" M% j! I7 U
- <launch>
) j" a/ h6 d% [/ q6 d6 z0 J0 ? - <node name="rplidarNode" pkg="rplidar_ros" type="rplidarNode" output="screen">
9 x1 }5 O1 U( j0 X9 C - <param name="serial_port" type="string" value="/dev/2d_lidar"/>; n/ I4 N. ~( p/ d
- <param name="serial_baudrate" type="int" value="115200"/><!--A1/A2 -->
3 W2 ^9 p0 j; ^8 Q; ~ - <!-- <param name="serial_baudrate" type="int" value="256000"/> --><!--A3 -->! \8 Q& s, Z |% O o) N% D7 R
- <!-- <param name="serial_baudrate" type="int" value="1000000"/> --><!--S2 -->
; S0 {; L5 V, ]1 o8 x - <param name="frame_id" type="string" value="laser"/>+ x# D' v$ W# H
- <param name="inverted" type="bool" value="false"/>' |% @# a+ |+ Z2 s# V8 ?8 x
- <param name="angle_compensate" type="bool" value="true"/> 3 o! F0 q* `4 T, ~. h4 u
- </node>3 a6 {" c; _$ w8 r& J7 ^; \
- </launch>
复制代码) b: B$ d- {9 Q) x9 d. ]
上面的代码中, - < param name=”serial_port” type=”string” value=”/dev/2d_lidar”/ >
复制代码 ` Q$ x& j6 _" z8 [9 K! o; A/ U
这里,我们就将系统的设备别名/dev/2d_lidar,设置到了参数serial_port中
+ i$ {1 t, e) f& ~9 H2 F; v
1 x' P6 W2 q, n3 W
ROS与STM32串口通信代码 0 V: A2 b2 K/ U, X) V/ s
这里以一个智能车代码工程为例,抽取串口通信部分代码 T3 u9 @: V8 C1 U( u# x
在头文件中,进行串口头文件的包含 - #include <serial/serial.h>
复制代码! ]7 z, _6 s' u. x
在类的定义中,什么一个 serial 类的实例 - serial::Serial Stm32_Serial; //声明串口对象
复制代码8 ?% d; v' A8 b
并且在类的定义中,声明两个结构体,用来存储接收和要发送的数据 - RECEIVE_DATA Receive_Data; //The serial port receives the data structure //串口接收数据结构体0 k$ u# c) c0 o2 s0 a6 f
- SEND_DATA Send_Data; //The serial port sends the data structure //串口发送数据结构体
复制代码. Z! Q% D" g" Y5 @ M7 P# _
在类的构造函数中,配置这个串口对象的参数 - private_nh.param<std::string>("usart_port_name", usart_port_name, "/dev/stm32_controller"); //Fixed serial port number //固定串口号
& {2 \8 ^7 f( N1 k - private_nh.param<int> ("serial_baud_rate", serial_baud_rate, 115200); //Communicate baud rate 115200 to the lower machine //和下位机通信波特率115200
复制代码' j. D, ~% Q; s4 i V3 G
这两个参数是在launch文件中设置的,代码里进行参数的读取。 ) T8 c8 w+ P- w
usart_port_name 设置的USB设备别名 $ f- \! m2 @1 u# c; q2 H
serial_baud_rate 串口通信的波特率要和stm32设置的一致
- p( n' a `5 S! e2 R0 j4 S. j( F
- try* E- a9 L+ S% L
- { 5 K4 D: w# o, ^! ?, ]: z0 ^
- //Attempts to initialize and open the serial port //尝试初始化与开启串口
; N5 K W& W- j! m) b - Stm32_Serial.setPort(usart_port_name); //Select the serial port number to enable //选择要开启的串口号" j/ R8 e/ R5 h: c' ~
- Stm32_Serial.setBaudrate(serial_baud_rate); //Set the baud rate //设置波特率
" i% R9 Q7 U6 [3 W% r: r - serial::Timeout _time = serial::Timeout::simpleTimeout(2000); //Timeout //超时等待% X. U4 I/ L/ `3 j: U
- Stm32_Serial.setTimeout(_time);
9 ]/ D) s) G6 d% s$ Y! T% G( D7 \ - Stm32_Serial.open(); //Open the serial port //开启串口% ?8 W8 W6 Z9 Y$ i, z$ i: f6 f! j/ } [
- }" X# F9 |& d- a. o' y3 J
- catch (serial::IOException& e)/ H/ I( a, d! O3 x
- {
6 V) K$ I/ s) ] - ROS_ERROR_STREAM("car_robot can not open serial port,Please check the serial port cable! "); //If opening the serial port fails, an error message is printed //如果开启串口失败,打印错误信息
' m3 Z" V0 f* d& [% { S5 V - L. g% D! _5 }
复制代码7 `% s1 _7 H) I; U+ Q' {+ }
初始化串口配置,并开启串口 6 Z3 [! Z( T$ H* @
设置的参数包括: 要开启的串口号 设置波特率 超时等待 9 `. `9 F4 V+ t; K% {5 s: i6 J; ^, N! |
6 r, E" m4 F4 L! n! O* Z( t& \# ?& n- j
判断串口是否被打开,打开输出终端打印信息 - if(Stm32_Serial.isOpen())7 i9 \% s7 ^8 [5 Q4 f
- {( ^& G" R; |' L9 [, V
- ROS_INFO_STREAM("car_robot serial port opened"); //Serial port opened successfully //串口开启成功提示 K9 i3 I& n* t4 @$ q Q7 Z
- }
复制代码3 v5 [7 m: k; \! c7 W
ROS主控读取stm32发送的数据 2 o4 f& M% s9 x
之后便可以通过 - Stm32_Serial.read(Receive_Data_Pr,sizeof(Receive_Data_Pr));
复制代码
( d* ]1 ]+ W" x9 k
read函数读取串口接收到的字节,之后通过定义的通信协议再进行和校验与数据解析即可stm32向ROS主控发送数据。 - i! h3 Q. b" ]+ b7 ~4 E
ROS主控向stm32发送数据
" \3 J% u0 _3 U
ROS主控向stm32发送数据的代码如下: . g* s6 h' S8 u7 L
将之前定义的发送数据的结构体 Send_Data的tx 中填入要发送的字节 - Send_Data.tx[0]=FRAME_HEADER; //frame head 0x7B //帧头0X7B
5 h$ }7 g) H2 R- O1 W$ b - Send_Data.tx[1] = 0; //set aside //预留位' b; i- L% ^! C. U- K% ^# s7 G
- Send_Data.tx[2] = 0; //set aside //预留位
复制代码
& c& S3 T; `2 l8 ?, @( N* h
填好字节后,直接通过下面代码发送即可
# f) b6 D. \9 n4 u- try
* x, N' o. \# m4 E" z6 C: f+ B - {: ]% C7 p( P7 Z, v2 n( \: ^
- Stm32_Serial.write(Send_Data.tx,sizeof (Send_Data.tx)); //Sends data to the downloader via serial port //通过串口向下位机发送数据
7 o& _, ^/ T6 b! P1 r) ? \. F - }* @9 A9 ]/ a. i" Y
- catch (serial::IOException& e) / y, x% H5 V, G) r) P
- {/ A) P" {+ z* `1 I
- ROS_ERROR_STREAM("Unable to send data through serial port"); //If sending data fails, an error message is printed //如果发送数据失败,打印错误信息
! O! M/ |0 o& l) J# ]6 c/ C - }
复制代码 |