* L1 m1 Y ~, k* ~ Z
; W' H3 b" e% h( O5 k6 R; I以简单的智能车为例,一般会存在两个控制器,一个是运行ROS的主控,另一个是运行电机控制和传感器信息采集的单片机比如STM32。
( a5 g! n- G3 ~( G. R / ?! ~4 k$ d2 [- v G7 v
) j* i' f8 L& [5 c+ C
由于存在多个控制器,完成一个机器人的具体任务,那么这多个控制器间则需要建立通信,本篇博客主要讲解: ( L/ g' c a( ?' u
如何实现ROS主控和STM32之间的通信 ROS主控对STM32发送过来的数据做哪些处理 ( z. P! t& W3 |- ~
h4 \/ o1 ], ~% m1 ~6 |) I
以智能车的应用例程展开
, v" ^4 u# {. h, z
! A/ d/ m, `; o* b
智能车控制器功能 在智能车里存在两个控制器: ROS主控 STM32控制器 1 Q/ s; o! W" i7 @
, }+ I- l3 M2 X- t) t5 [" s# p
只要能将ROS跑起来就可以作为ROS主控,ROS主控可以是: jetson 系列,例如 nano、tx、nx 树莓派 工控机 ! c! J) Q% g, N
" P- u9 w; r a7 f% @7 B
在智能车里,ROS主控主要实现的功能有: 雷达信息采集 摄像头信息采集 路径规划 定位 5 K* N3 c! x2 w6 P1 B! z) d3 J
% k! z% L3 E' j' }1 @2 y6 J
STM32 控制器主要实现的功能有: 里程计信息采集 陀螺仪信息采集 电机控制
B9 t0 S3 f" o5 |5 M
_$ ~9 @9 c6 \. [4 d: |* A% Z$ Y
通信内容
4 o! x( @+ S- A9 H8 Y' u
0 E' j2 z Z: b* }5 Z# p2 g3 X* w) T! T$ \& ?9 I! E
ROS主控负责接收stm32发送过来的传感器数据
; T+ ~1 s4 D0 r
数据有里程计、imu、电池电压。其中里程计就是电机的转速,通过编码器采集到。
! z3 R6 [+ [$ P9 h) k2 Q
STM32负责接收ROS主控发送过来的运动底盘的目标速度,STM32再完成电机转速的控制,最终实现小车的移动任务
7 m$ _" p8 t: n) m. ?/ A
ROS主控与STM32之间需要做到一个双向的数据传输,这里就涉及到了两个控制器之间的通信问题,下面则介绍如何实现两者之间的通信 5 Y% N% b" l$ M: y) b- {& ~; m5 E' e
+ m( a/ K l" {2 v/ A
硬件连接
. l$ D& N2 J/ }; W% k5 e4 q + x: d9 S5 g( ^- U: n2 h7 c! e7 K
8 P) S+ B8 i) o7 f$ e" y" ]. `
ROS主控通过usb线连接到一个TTL电平转换芯片,再由这个电平转换芯片连接STM32芯片
- X6 c& `, o6 M/ O1 i# R5 u
电平转换芯片可以通过PCB设计在STM32芯片的电路板上,也可以使用一个USB转TTL的模块。 3 W2 K/ G" b- |( d
' y' M3 @, Y8 H( J, h, Z' y( q* V' L3 n) C1 e- I% A
: b# P; |! O2 l3 T" A* E# T
/ p/ G+ i) a% c; V1 ~( f1 Z
为什么两个控制器之间需要电平转换芯片?
$ T+ d0 ~, O8 j9 G因为两个控制器之间通信层次逻辑是不同的,所有需要电平转换芯片。相当于两个主控是两种不同语言的人,电平转换芯片相当于一个翻译。
8 q( C+ T K% @3 u+ ?, h! Q! ]( }
电平转换芯片可以是: cp2102 ch340 PL2303 FT232RL 9 }2 A6 C2 ?) H z
( K& Q& l W& g+ X7 F4 g3 V* T; t
3 K" X# C9 Z: J! K' X
软件设置 硬件连接上之后,需要一个软件设置 ; H7 o$ V$ ?% F0 L/ d, L
需要软件设置原因: ROS主控可能接入多个USB设备,或者接入两个型号一样的电平转换芯片。 5 `* G- d1 f$ [) P+ X
不同USB设备占用的ROS主控的端口号在每次上电时可能会不一致,这样需要手动修改代码中的配置参数,比较麻烦,也无法做到自启动。
- h* |1 v7 S* |( {$ m+ P+ Y) [& [1 v如果存在多个USB设备,但是每种USB设备的电平转换芯片不一样,那么我们可以根据芯片名称来知道端口 号,但是如果有两个芯片一样的电平转换芯片,则无法区分,这时候想做自启动那么必须要进行下面步骤的软件设置
- l4 \$ e* @" [# t
软件设置分为两步: 第一步是更改电平转换芯片的serial, 第二步是创建设备别名
0 J% `9 a6 ?1 m8 Q1 H
& R1 b+ \6 Y! B% P1 v+ |
更新电平转换芯片的serial
: X! c5 X1 e. ?7 ]; p, ? P
首先在win环境下安装更改芯片serial的软件
! L& }3 u1 Z# q. x$ v/ W) SCP21xx Customization Utility.exe
3 }6 y+ U5 H0 ]9 n. L
这个软件在网上下载就可以
r) S) x/ A* a* q打开这个软件,然后将芯片连接电脑的USB 5 a6 m( {1 O9 b
+ b O. b, v* ~$ b- \/ W
) n& z( z1 j% L6 i( [0 r
然后将圆圈位置改为0002 ) U- m& ^+ \% |' e. X. G5 r
然后点击Program Device
: E5 W {0 T8 `/ |7 H, X* {5 n S3 @) i6 {6 t9 {0 u$ Y0 F& |1 o5 p
9 R7 b& n9 k1 F6 Y( h; @
点完之后要等下,在Status Logging窗口中出现下面信息,才说明修改好了 ! F l* L) H# U" d5 t
" r0 c" C+ e4 w0 Z$ f2 U
; d1 ]8 h' Q& L5 i/ L
创建设备别名 4 k( [. `7 r n! Y& O, e
需要创建设备别名原因:
# E& w4 O- V; y- \在运行一个ros程序的时候需要提供一个端口名,这个端口名一般是ttyUSBx,设备每次插拔对应的这个端口名它都会不一样,需要创建一个设备别名,就是要将这个端口名来给它固定住。
9 t- O6 q) e# W C. y7 T. ]* K
8 R d! T0 W# f8 U0 R* |
3 o8 x0 B1 X5 \7 s0 u5 n. `1 V' `
重新插拔 USB1端口的设备后,变为: ! m! {# R$ N' e# S# b
5 Q* G+ ^' Z+ @. e/ \& s: h( L+ \$ [5 [8 S4 V
可以看到变成了/dev/ttyUSB2 0 ]% ?$ F4 `: ~
端口号发生了变化 ) q5 C1 C7 c) G# e7 Q) m- x
创建设备别名需要写一个脚本文件,如下: - 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
# ^' o6 n+ v4 J; u" I4 q - 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
U7 ]7 t0 R) v. _# p - 1 D0 v# r( N2 \: V/ ]0 @
- service udev reload: E0 j( f- d6 R* W6 P5 A
- sleep 21 A9 W, b. b1 J9 x
- service udev restart
复制代码8 C" ~4 \3 X, U: e5 A
解释下上面的代码 . g0 h! R" @! d( L* p
KERNEL==”ttyUSB* 不管是USB几的设备都进行判断 & d) A* y/ R% U$ h& h, Z" f% R
ATTRS{idVendor}==”10c4” 这里的idVendor ,在前面win上修改 serial的时候出现过,就是10c4
& Q4 C( k0 O- _2 v/ r) {. K
' ^: W( @/ K* x9 c* o$ c; B# G* m
ATTRS{idProduct}==”ea60” 这里的idProduct,在前面win上修改 serial的时候出现过,就是ea60
7 |1 _3 j b" s$ C
6 s) u" l2 z q6 h! v/ D" y# G1 o/ Z, A4 y' {
ATTRS{serial}==”0002” 这里的serial,就是前面我们改过的,这里就根据这个值的不同,定义不同的设备别名
$ F g' Q" Z$ U9 r4 P+ N. s) _MODE:=”0777” 就是端口的权限
) f- @: `- O- g2 P
SYMLINK+=”stm32_controller” 这里的stm32_controller,就是取的设备别名。
) y7 h3 C; ?3 A# @" b2 P2 u
所有上面第一行代码的功能就是,将满足这些条件的端口的设备名称改为定义的设备别名。 2 H- I: J [! d7 r+ ~. ` N$ L1 L
脚本的文件名称,取名为change_udev.sh。在执行前需要给这个脚本文件赋予权限。 - sudo chmod 777 change_udev.sh
复制代码' G; {8 z5 t. E# C1 i
赋予权限后,再运行这个脚本
7 O' g1 Z' D0 S b2 A
这样就运行了设备别名的设置,之后不管怎么插拔这两个USB设备,系统都将会自动的将这两个设备去给它赋予设置的设备别名。 ) g4 C+ J: a6 }8 T
使用设备别名 ; N& u4 t7 j. g; t1 i9 _
在上面,设置了设备别名,下面来看如何使用我们的设备别名。
! y' O# h% A8 t0 P9 \5 M9 s4 f( O
例如我们将雷达的USB的设备别名改为了2d_lidar
' o" z/ |9 X9 y5 t0 s# b
雷达的roslaunch启动文件则可以写成如下: 8 ?; d4 E4 i. K/ L) Z
- <launch>
' l f x I, t4 J6 ^! W - <node name="rplidarNode" pkg="rplidar_ros" type="rplidarNode" output="screen">8 c2 F: e# ^4 M+ j
- <param name="serial_port" type="string" value="/dev/2d_lidar"/>
. Z* j% h6 T; T% q; i5 Q' p - <param name="serial_baudrate" type="int" value="115200"/><!--A1/A2 -->$ h0 v2 e' [# f* I8 {
- <!-- <param name="serial_baudrate" type="int" value="256000"/> --><!--A3 -->
S4 ?4 `# n( S. M - <!-- <param name="serial_baudrate" type="int" value="1000000"/> --><!--S2 -->
# t8 A+ B! M% M2 Z - <param name="frame_id" type="string" value="laser"/>5 Q' \* {7 B/ c+ W2 Y) P, I5 d
- <param name="inverted" type="bool" value="false"/>8 {# {0 Z6 M/ k7 `. W
- <param name="angle_compensate" type="bool" value="true"/>
9 X9 u8 ]. x0 v+ O) E- z+ h - </node>
; ^" y- x- ~" E2 @: ?- @ - </launch>
复制代码! H* Z4 x' ?, ]. G
上面的代码中, - < param name=”serial_port” type=”string” value=”/dev/2d_lidar”/ >
复制代码
6 Z* `' s0 r( _
这里,我们就将系统的设备别名/dev/2d_lidar,设置到了参数serial_port中
& w/ R: x6 v" ~1 z5 Z$ ^
( Y+ t' v$ G* R) c
ROS与STM32串口通信代码 & _$ d& Z. i: r/ b3 Y* t7 x
这里以一个智能车代码工程为例,抽取串口通信部分代码 $ p; o, {5 R* ]8 W
在头文件中,进行串口头文件的包含 - #include <serial/serial.h>
复制代码
1 y% D, h2 S M, t
在类的定义中,什么一个 serial 类的实例 - serial::Serial Stm32_Serial; //声明串口对象
复制代码% w5 J/ P6 J( y
并且在类的定义中,声明两个结构体,用来存储接收和要发送的数据 - RECEIVE_DATA Receive_Data; //The serial port receives the data structure //串口接收数据结构体
' g& m) M" f5 L t3 z - SEND_DATA Send_Data; //The serial port sends the data structure //串口发送数据结构体
复制代码$ J% d! x) r- E+ A4 I
在类的构造函数中,配置这个串口对象的参数 - private_nh.param<std::string>("usart_port_name", usart_port_name, "/dev/stm32_controller"); //Fixed serial port number //固定串口号( B+ D- s* t2 E
- private_nh.param<int> ("serial_baud_rate", serial_baud_rate, 115200); //Communicate baud rate 115200 to the lower machine //和下位机通信波特率115200
复制代码
+ I7 {* U( f. Z; x' h
这两个参数是在launch文件中设置的,代码里进行参数的读取。 5 c; v q+ A1 \7 d
usart_port_name 设置的USB设备别名 . H4 ?' s: u$ E& N, R5 Y
serial_baud_rate 串口通信的波特率要和stm32设置的一致
1 G K3 K# r; P5 S+ g/ D2 b
- try% x# M8 ^, `. u2 i4 w
- { + G$ f( C; a D' o' ?: l9 \
- //Attempts to initialize and open the serial port //尝试初始化与开启串口, n4 f* w# C, t0 V( I& \7 ?2 u
- Stm32_Serial.setPort(usart_port_name); //Select the serial port number to enable //选择要开启的串口号1 Y7 K, \2 y9 M' t7 ~: P
- Stm32_Serial.setBaudrate(serial_baud_rate); //Set the baud rate //设置波特率
: s, i0 [. ^+ I: | - serial::Timeout _time = serial::Timeout::simpleTimeout(2000); //Timeout //超时等待
- r+ O) o* Q0 k8 ? - Stm32_Serial.setTimeout(_time);( M* t H( f2 q, R" c
- Stm32_Serial.open(); //Open the serial port //开启串口
# ]. A( m0 `# ] r# r - }; [% n2 I& p0 l7 D
- catch (serial::IOException& e)
2 R/ C. n' u9 ^. P- E8 x - {
7 Y- o% O2 `: Y: P' b0 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 //如果开启串口失败,打印错误信息
6 O# h2 m; S4 _3 j7 X( M - 3 h' S) |, N! l5 m
复制代码4 H. C+ ^2 W6 V* O
初始化串口配置,并开启串口
* |, w4 ] J, ?6 x6 v设置的参数包括:
要开启的串口号 设置波特率 超时等待 5 m) y- Z" S/ y& M: w$ e
/ [8 b' l/ }8 E4 D
判断串口是否被打开,打开输出终端打印信息 - if(Stm32_Serial.isOpen())0 w7 O+ q" |% _# a/ S" D
- {
1 L/ [9 X( N9 P( V6 K - ROS_INFO_STREAM("car_robot serial port opened"); //Serial port opened successfully //串口开启成功提示% s1 u8 ?/ c/ }) V3 q
- }
复制代码9 B7 u- t- S9 W$ {. k/ W7 x
ROS主控读取stm32发送的数据 ) p, R7 w- P( {$ h" L( S5 O
之后便可以通过 - Stm32_Serial.read(Receive_Data_Pr,sizeof(Receive_Data_Pr));
复制代码) T% H3 h& Q" o
read函数读取串口接收到的字节,之后通过定义的通信协议再进行和校验与数据解析即可stm32向ROS主控发送数据。
' s4 c' K k6 z) N
ROS主控向stm32发送数据
4 h* }' U- w# ~% H
ROS主控向stm32发送数据的代码如下: + `: E& Q6 E* |! M% n
将之前定义的发送数据的结构体 Send_Data的tx 中填入要发送的字节 - Send_Data.tx[0]=FRAME_HEADER; //frame head 0x7B //帧头0X7B$ z) C! V+ A% H9 h; e
- Send_Data.tx[1] = 0; //set aside //预留位; W: g& W j$ G% B n4 ?
- Send_Data.tx[2] = 0; //set aside //预留位
复制代码
* E+ ~5 I7 x3 O% g
填好字节后,直接通过下面代码发送即可 6 Z% N5 ^1 E: w' I4 }2 b- ]2 j
- try
# ]4 T' o8 s: o - {% l) F! v4 H6 `" E! r5 t
- Stm32_Serial.write(Send_Data.tx,sizeof (Send_Data.tx)); //Sends data to the downloader via serial port //通过串口向下位机发送数据 + A1 ~7 c) n9 Z5 z0 M6 e
- }# j# |3 C! L7 X# Z4 n, `
- catch (serial::IOException& e)
9 q7 U0 K; u3 M$ c R$ Y; b - {
e+ V N$ u l3 s5 _" I - ROS_ERROR_STREAM("Unable to send data through serial port"); //If sending data fails, an error message is printed //如果发送数据失败,打印错误信息( U ^- S, r3 C; f( {3 Q
- }
复制代码 |