8 |3 K! Y/ z- w* l' @' F# ?
3 X, f6 A3 g. E: |以简单的智能车为例,一般会存在两个控制器,一个是运行ROS的主控,另一个是运行电机控制和传感器信息采集的单片机比如STM32。
3 n! h0 y0 v. a
5 b* _: q# M# e/ I8 w) d( @# i3 ?- t# J1 `, a: y
由于存在多个控制器,完成一个机器人的具体任务,那么这多个控制器间则需要建立通信,本篇博客主要讲解:
- |& K: ]7 f9 J5 z8 L8 i. m
如何实现ROS主控和STM32之间的通信 ROS主控对STM32发送过来的数据做哪些处理
* p, {: A( u% N$ D7 X
# n5 y( p# w- N8 P7 O- c( ?
以智能车的应用例程展开 ; \( F4 p4 W c2 ?; q0 Q; d) q
2 q r. P3 g) t
智能车控制器功能 在智能车里存在两个控制器: ROS主控 STM32控制器 7 Y$ L" i9 l( ?4 K8 {
4 G x1 p/ N, d8 P* R
只要能将ROS跑起来就可以作为ROS主控,ROS主控可以是: jetson 系列,例如 nano、tx、nx 树莓派 工控机 / X* C* }; `* @* ?( [
/ i! c8 ]- @/ @; F' V! J/ t
在智能车里,ROS主控主要实现的功能有: 雷达信息采集 摄像头信息采集 路径规划 定位
2 J! `8 y3 G5 K
8 c2 G+ v# l& H6 A
STM32 控制器主要实现的功能有: 里程计信息采集 陀螺仪信息采集 电机控制 # A; F* C2 B: }
) e4 f* P2 t8 y6 E% D
通信内容 ( |9 g' L2 X* } q6 L( y
" ~+ c' v% `! q4 o: f3 n3 v
& l0 M1 @6 ?$ c- M
ROS主控负责接收stm32发送过来的传感器数据
4 _2 P- i+ D2 Y8 J$ i6 B
数据有里程计、imu、电池电压。其中里程计就是电机的转速,通过编码器采集到。
' e- e7 ?+ G: T( H. y
STM32负责接收ROS主控发送过来的运动底盘的目标速度,STM32再完成电机转速的控制,最终实现小车的移动任务 ( L* Q% i) w5 u; A/ J y3 q; t
ROS主控与STM32之间需要做到一个双向的数据传输,这里就涉及到了两个控制器之间的通信问题,下面则介绍如何实现两者之间的通信
0 h3 L/ h/ B$ e3 B9 ^% I
% @( b6 \! H8 Q6 ~( B1 ^
硬件连接 . T2 j4 ?3 W% c7 m
& ^; W2 ~4 V0 q7 o8 {4 S& a' f' n6 @3 U
ROS主控通过usb线连接到一个TTL电平转换芯片,再由这个电平转换芯片连接STM32芯片 : i. L" O$ c% ~2 O( i# i e8 o
电平转换芯片可以通过PCB设计在STM32芯片的电路板上,也可以使用一个USB转TTL的模块。
; U$ X& x5 G2 e# s& [7 J+ q; D1 e9 `9 B$ a9 C+ {8 }
* D4 ^: \" V) c- [$ _1 X7 C1 k; e: y8 P2 v5 E" O$ G
$ ^: \1 P4 S' ^7 }, C5 I为什么两个控制器之间需要电平转换芯片? 0 w6 Z8 X J7 E: J& x" p9 p0 `
因为两个控制器之间通信层次逻辑是不同的,所有需要电平转换芯片。相当于两个主控是两种不同语言的人,电平转换芯片相当于一个翻译。
& x* k6 F+ X8 S: E2 ]
电平转换芯片可以是: cp2102 ch340 PL2303 FT232RL
. `" {# `# h, N2 y) L
# r) M0 P) r) A/ t& z, ]
, w1 ^ }8 [& P7 S. S. a, _/ _
软件设置 硬件连接上之后,需要一个软件设置
5 V3 p2 B4 ^ Y7 m: T
需要软件设置原因: ROS主控可能接入多个USB设备,或者接入两个型号一样的电平转换芯片。
y* v% ?* K$ [7 L+ Z; w$ }. C7 V不同USB设备占用的ROS主控的端口号在每次上电时可能会不一致,这样需要手动修改代码中的配置参数,比较麻烦,也无法做到自启动。
& Z# w" k* C! p; C6 I2 \0 D5 A
如果存在多个USB设备,但是每种USB设备的电平转换芯片不一样,那么我们可以根据芯片名称来知道端口 号,但是如果有两个芯片一样的电平转换芯片,则无法区分,这时候想做自启动那么必须要进行下面步骤的软件设置 % P: Y3 s. ]" }6 r
软件设置分为两步: 第一步是更改电平转换芯片的serial, 第二步是创建设备别名
& }/ |- J7 P4 o a) N6 b$ v
9 z. R$ F1 H4 a8 t1 j* q/ m$ ^2 n
更新电平转换芯片的serial
9 F; \! m4 W$ b. [8 C6 @
首先在win环境下安装更改芯片serial的软件
. C* J; L6 @, N1 uCP21xx Customization Utility.exe
" Q, Q3 N' R5 u8 `- y( N
这个软件在网上下载就可以 ; ~+ G" I. b' V5 P
打开这个软件,然后将芯片连接电脑的USB
/ m8 [/ y& C3 v& Q" c
5 `( i9 B. y- V; M7 Z8 q! E8 k7 P, k% p. m Y
然后将圆圈位置改为0002 / q9 q, ]% ?3 A, l7 ?: P& ^1 x
然后点击Program Device 1 f8 N3 Z* }/ \# \/ L9 Q
W- ^9 x; a8 W& G; ]
8 y# F! V% h t7 r. a9 E5 [
点完之后要等下,在Status Logging窗口中出现下面信息,才说明修改好了 ! ]$ A2 o. j* u* i) i
' v+ c* S" Q# x% v) ~& G! n, S f 0 C, ]- f9 T/ @' @/ \
创建设备别名 ( ~( F6 r: _2 h+ j
需要创建设备别名原因:
. P Z7 @1 i" J1 h在运行一个ros程序的时候需要提供一个端口名,这个端口名一般是ttyUSBx,设备每次插拔对应的这个端口名它都会不一样,需要创建一个设备别名,就是要将这个端口名来给它固定住。
. Q* x( U. V! m, b+ Q; J1 q
7 Z# h9 X/ e8 _
3 v! {4 J3 [7 V
重新插拔 USB1端口的设备后,变为:
' d3 e# S" j# P7 _+ k) Q
) C0 ~" b% o% {0 T0 W
1 w& W8 U) r4 M4 o
可以看到变成了/dev/ttyUSB2
- I: P- _2 N6 H/ ?( H端口号发生了变化
1 j3 P4 b; b& b# w/ [
创建设备别名需要写一个脚本文件,如下: - 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
2 J/ m9 ~* \) a( n; l4 n2 O - 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
$ W; U3 M* I) Y7 w- B
2 M4 t' V* m6 r( }2 m" E- service udev reload
5 S; n4 G, ^: g- C% Y - sleep 2
4 m% J& N' C5 g# y' l8 `( n2 Z - service udev restart
复制代码# ~! q6 z. T$ F( Q
解释下上面的代码
$ O/ V! A5 n3 }7 S0 D/ yKERNEL==”ttyUSB* 不管是USB几的设备都进行判断
8 B, g1 o* Y3 }' d1 b6 [0 c7 x3 }6 nATTRS{idVendor}==”10c4” 这里的idVendor ,在前面win上修改 serial的时候出现过,就是10c4
- R% o4 N; ~# o; K8 ^& R5 u$ S& f
* M% a, ~3 v% G O% H
, M7 c0 \4 V% w- K# Z0 c
ATTRS{idProduct}==”ea60” 这里的idProduct,在前面win上修改 serial的时候出现过,就是ea60
& L% L' X) T" q; F" y
. \/ a! Z- a: p8 z( B; L1 z! X$ `. q4 S! m1 c6 Z
ATTRS{serial}==”0002” 这里的serial,就是前面我们改过的,这里就根据这个值的不同,定义不同的设备别名
- M' s& d* q$ WMODE:=”0777” 就是端口的权限
# U7 L0 r9 q8 e% @9 V: l& ASYMLINK+=”stm32_controller” 这里的stm32_controller,就是取的设备别名。
, C" j, q+ j/ j" |' |4 F3 }
所有上面第一行代码的功能就是,将满足这些条件的端口的设备名称改为定义的设备别名。 " v" H6 z4 g) w0 C2 B0 L
脚本的文件名称,取名为change_udev.sh。在执行前需要给这个脚本文件赋予权限。 - sudo chmod 777 change_udev.sh
复制代码
) G/ J; J/ V! ?" o; G$ D$ q
赋予权限后,再运行这个脚本
! J+ Y% T. C7 S; Q+ n) F
这样就运行了设备别名的设置,之后不管怎么插拔这两个USB设备,系统都将会自动的将这两个设备去给它赋予设置的设备别名。 9 F @9 k/ {) A/ Z6 D6 l M; T
使用设备别名 $ K3 Z* ^: E8 s9 ^' m6 j9 g
在上面,设置了设备别名,下面来看如何使用我们的设备别名。 1 i. ~$ \' I0 c9 v0 G
例如我们将雷达的USB的设备别名改为了2d_lidar
) T+ ]2 L" |4 {6 {& l
雷达的roslaunch启动文件则可以写成如下:
# v: D% t& N! ~4 t! Z! H1 k
- <launch>, ~3 f) F0 t0 y* W" @+ |% q
- <node name="rplidarNode" pkg="rplidar_ros" type="rplidarNode" output="screen">' E$ L+ Q( E& E+ e; K, ^6 n6 k6 p
- <param name="serial_port" type="string" value="/dev/2d_lidar"/>; p7 [1 S: I- L1 n+ i4 T
- <param name="serial_baudrate" type="int" value="115200"/><!--A1/A2 -->2 h# y' E: i9 O
- <!-- <param name="serial_baudrate" type="int" value="256000"/> --><!--A3 -->
0 T6 z; v- |1 w/ ~* j. j5 ] - <!-- <param name="serial_baudrate" type="int" value="1000000"/> --><!--S2 -->6 `1 J. ?* v1 q4 c# q4 f6 W
- <param name="frame_id" type="string" value="laser"/>6 Y: X6 M: [, Z; V
- <param name="inverted" type="bool" value="false"/>
4 X' x, |% V5 _; ~ `9 s, w5 S - <param name="angle_compensate" type="bool" value="true"/>
7 u/ U3 l2 ~& `: g3 J! W7 ~; I0 L# Z - </node>/ I& B$ k$ p8 K
- </launch>
复制代码/ T5 F7 P% p$ J: A+ K7 B0 r8 F* N
上面的代码中, - < param name=”serial_port” type=”string” value=”/dev/2d_lidar”/ >
复制代码
' E, a2 j4 L3 f+ l+ w( M
这里,我们就将系统的设备别名/dev/2d_lidar,设置到了参数serial_port中
# j; D. X L* W, v% B
9 [7 Z8 ?0 s0 y( U* G
ROS与STM32串口通信代码
6 U3 t' S K4 V- w
这里以一个智能车代码工程为例,抽取串口通信部分代码
- Z+ C7 t9 i, C, w
在头文件中,进行串口头文件的包含 - #include <serial/serial.h>
复制代码
- F3 e6 A, u0 g2 c0 H
在类的定义中,什么一个 serial 类的实例 - serial::Serial Stm32_Serial; //声明串口对象
复制代码
% W0 k0 K8 I& o2 X! b* p( h
并且在类的定义中,声明两个结构体,用来存储接收和要发送的数据 - RECEIVE_DATA Receive_Data; //The serial port receives the data structure //串口接收数据结构体
* Z) ]" j$ ]& x( j$ n - SEND_DATA Send_Data; //The serial port sends the data structure //串口发送数据结构体
复制代码1 W! _3 ]. t1 z
在类的构造函数中,配置这个串口对象的参数 - private_nh.param<std::string>("usart_port_name", usart_port_name, "/dev/stm32_controller"); //Fixed serial port number //固定串口号 g8 a$ n5 e8 V7 [
- private_nh.param<int> ("serial_baud_rate", serial_baud_rate, 115200); //Communicate baud rate 115200 to the lower machine //和下位机通信波特率115200
复制代码
( S+ A0 d4 ~8 E. s u" [
这两个参数是在launch文件中设置的,代码里进行参数的读取。
4 C' X/ n, D/ [: uusart_port_name 设置的USB设备别名
$ E4 B% c$ y; F' L+ _serial_baud_rate 串口通信的波特率要和stm32设置的一致
0 U; S" H- @& T# h% u. R
- try" J' d; U& j ~% d. X% ?4 P
- { ; E- o0 O3 K: x% q, _ `/ m
- //Attempts to initialize and open the serial port //尝试初始化与开启串口- ]3 |4 }( Z* l; u: }6 T, G
- Stm32_Serial.setPort(usart_port_name); //Select the serial port number to enable //选择要开启的串口号
3 M8 Z4 N4 Q" z: E - Stm32_Serial.setBaudrate(serial_baud_rate); //Set the baud rate //设置波特率9 W+ @* _, g! J+ ]' y4 s7 @
- serial::Timeout _time = serial::Timeout::simpleTimeout(2000); //Timeout //超时等待* O6 C' U# ^$ C5 S/ l
- Stm32_Serial.setTimeout(_time); F& M0 B0 F5 n( q, t3 b0 z9 U; E
- Stm32_Serial.open(); //Open the serial port //开启串口
+ R( C* ]5 h/ }' ~/ _, v6 } - }
2 o$ D( A2 I: e4 { - catch (serial::IOException& e): }: P# o0 F, ?1 N( e4 O7 ^/ z
- {
; Q; }6 M' \" B- c: {" | - 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 //如果开启串口失败,打印错误信息2 ~3 v0 ]; e) p' t. r
' Z8 s# y, Q$ O7 }. g
复制代码
4 q' A, G$ g1 m* h. g
初始化串口配置,并开启串口
& q* M7 U1 h4 n, o1 `- H( n设置的参数包括:
要开启的串口号 设置波特率 超时等待 : d2 G! L3 I: p; L9 J* y
* V) M5 y) l7 c* G& X$ Y
判断串口是否被打开,打开输出终端打印信息 - if(Stm32_Serial.isOpen())+ {% l! C9 \/ T7 ?2 K9 Q4 N
- {! S! k6 Z/ C& E8 a- `
- ROS_INFO_STREAM("car_robot serial port opened"); //Serial port opened successfully //串口开启成功提示
, f% L: D) W, o T5 v - }
复制代码0 k( W+ F' V0 N8 M p
ROS主控读取stm32发送的数据 1 F: |4 j4 F `: r1 T: |" p
之后便可以通过 - Stm32_Serial.read(Receive_Data_Pr,sizeof(Receive_Data_Pr));
复制代码
- b% f0 D! n; _6 ]$ t
read函数读取串口接收到的字节,之后通过定义的通信协议再进行和校验与数据解析即可stm32向ROS主控发送数据。
( I5 \' ?) ?+ \8 h
ROS主控向stm32发送数据 ! m2 G+ V# {3 ]: y6 \
ROS主控向stm32发送数据的代码如下: ( Q. C) T0 V) l+ z6 `# s% Y
将之前定义的发送数据的结构体 Send_Data的tx 中填入要发送的字节 - Send_Data.tx[0]=FRAME_HEADER; //frame head 0x7B //帧头0X7B; ]0 O* P" w, `% e3 ? _" @- V* c- v
- Send_Data.tx[1] = 0; //set aside //预留位
: S! p: W$ c' E$ r' d& y - Send_Data.tx[2] = 0; //set aside //预留位
复制代码
, n K* A/ U& T! w
填好字节后,直接通过下面代码发送即可
9 Q- o( f: s" s8 L( ~& v- try
2 H/ C7 w9 a+ G A$ y - {
. M& e. }% ^% V) X& W - Stm32_Serial.write(Send_Data.tx,sizeof (Send_Data.tx)); //Sends data to the downloader via serial port //通过串口向下位机发送数据 % }6 F; N% b! z! S- Q
- }
; D2 C. N u" o; T - catch (serial::IOException& e)
5 ^' K. L. ~2 y( z9 H& W2 u3 d% @: d - {/ p9 H) V" P, J
- ROS_ERROR_STREAM("Unable to send data through serial port"); //If sending data fails, an error message is printed //如果发送数据失败,打印错误信息5 I& V/ G4 H2 q }" Q
- }
复制代码 |