# y, o% l4 \8 z5 m
3 V j& @; g7 n4 g+ o6 Q
以简单的智能车为例,一般会存在两个控制器,一个是运行ROS的主控,另一个是运行电机控制和传感器信息采集的单片机比如STM32。 4 L2 p$ F; L% ^' I
% p. ^1 G3 Y/ c
+ N- H; N9 _" c, g( k" V E+ s8 @- I/ {
由于存在多个控制器,完成一个机器人的具体任务,那么这多个控制器间则需要建立通信,本篇博客主要讲解: 4 m8 V5 \1 b% ?" X0 K5 h& c U
如何实现ROS主控和STM32之间的通信 ROS主控对STM32发送过来的数据做哪些处理 " u( p8 c5 D- Z' }8 E2 s$ h6 v
0 h9 X* W' w. v$ f4 Y) m( G( J
以智能车的应用例程展开
. B+ e: D" R* X" S
( e% h$ |8 Z3 i& a. J* N: z
智能车控制器功能 在智能车里存在两个控制器: ROS主控 STM32控制器
7 _1 h" Y3 Z/ F% C: u, m1 v( \" B
9 Q; G8 a# v$ \" u. C2 j1 ^
只要能将ROS跑起来就可以作为ROS主控,ROS主控可以是: jetson 系列,例如 nano、tx、nx 树莓派 工控机 : A3 {2 `" I1 U% t$ ?! }0 F
}2 |0 ]1 v) }% _
在智能车里,ROS主控主要实现的功能有: 雷达信息采集 摄像头信息采集 路径规划 定位
: h& M6 m( y" ?) e* i
4 Q0 ] U- s- y! Q
STM32 控制器主要实现的功能有: 里程计信息采集 陀螺仪信息采集 电机控制
" u7 G, B" U" G. E& ^
+ ]9 w* M2 a( C* Q) L) A5 s
通信内容
3 i, S4 q& m- e, t 0 W& n8 D! o. [* w4 o* @
9 q& w# } k+ i+ Z
ROS主控负责接收stm32发送过来的传感器数据
: L7 r$ k; q& C: [1 _
数据有里程计、imu、电池电压。其中里程计就是电机的转速,通过编码器采集到。 ' t* _, }/ S. o. E0 U8 N. \
STM32负责接收ROS主控发送过来的运动底盘的目标速度,STM32再完成电机转速的控制,最终实现小车的移动任务
/ c( ]/ i6 |- ~# H
ROS主控与STM32之间需要做到一个双向的数据传输,这里就涉及到了两个控制器之间的通信问题,下面则介绍如何实现两者之间的通信 ! S' ~9 C& R% i+ e4 |, r
, v) H* S7 a: E. p+ }6 c
硬件连接 : A Z' j" T# r) \" C7 ]# O; o
( ]+ o- [' f' e
% ~# b3 u" |& Q: h
ROS主控通过usb线连接到一个TTL电平转换芯片,再由这个电平转换芯片连接STM32芯片
; J3 u% P: u! p: f
电平转换芯片可以通过PCB设计在STM32芯片的电路板上,也可以使用一个USB转TTL的模块。
; N) p1 H" T% R" ]& w4 E& b+ g/ b8 ]6 h
. X2 F0 F9 ^ q' k; P' Y! U8 g, {
" c: g" E# U1 M; _6 {3 i! `为什么两个控制器之间需要电平转换芯片?
- r' o- r3 q: X! i- B! |& U0 f8 r因为两个控制器之间通信层次逻辑是不同的,所有需要电平转换芯片。相当于两个主控是两种不同语言的人,电平转换芯片相当于一个翻译。
! T. n2 ?$ w& _6 q
电平转换芯片可以是: cp2102 ch340 PL2303 FT232RL X6 C3 }5 q( @$ f4 B$ n
1 m4 q5 K# a& J+ P2 T) }
. N! j, X6 _* t# H/ ?) G4 |
软件设置 硬件连接上之后,需要一个软件设置
$ g" K1 Y: \: F& }2 y( K
需要软件设置原因: ROS主控可能接入多个USB设备,或者接入两个型号一样的电平转换芯片。
$ Y2 C) l: U5 r不同USB设备占用的ROS主控的端口号在每次上电时可能会不一致,这样需要手动修改代码中的配置参数,比较麻烦,也无法做到自启动。
m! y }2 `+ C6 @; @$ u/ D+ g2 P3 i如果存在多个USB设备,但是每种USB设备的电平转换芯片不一样,那么我们可以根据芯片名称来知道端口 号,但是如果有两个芯片一样的电平转换芯片,则无法区分,这时候想做自启动那么必须要进行下面步骤的软件设置
w/ O0 @) f) d i4 f6 G
软件设置分为两步: 第一步是更改电平转换芯片的serial, 第二步是创建设备别名
2 B2 J! Y1 w' b" r( f3 A- T' T) n
. R0 v$ Z& _/ J t5 z4 t- M7 q
更新电平转换芯片的serial ( K, I$ t1 \2 ~0 e# `7 b6 t, s" ~
首先在win环境下安装更改芯片serial的软件 & z- ]) {" ^1 @2 _6 H0 b7 E }$ `! G
CP21xx Customization Utility.exe 3 l: h& w( J( y* A" D, s
这个软件在网上下载就可以
' Z" U- }1 l9 R: X( Z, E$ ?7 L打开这个软件,然后将芯片连接电脑的USB , r" f4 @ S# F" t6 G' t
1 S8 `8 ?' R/ n/ T7 {" o8 X
8 l- i% f5 V) Z+ e2 S1 [( z) o& b
然后将圆圈位置改为0002
N+ f) f5 X7 k
然后点击Program Device
; J8 V" T: R! n9 ?/ x J1 e z) |: O$ n' G
/ G% O d0 B L- X* y9 _: k
点完之后要等下,在Status Logging窗口中出现下面信息,才说明修改好了
1 K0 `" P; h9 f& H7 E2 {$ ^" d/ W" e: b0 v2 ]4 }
: ~. ~* V, P1 c3 e' ?8 f F1 x
创建设备别名 ' P9 C" o& [3 m3 F! Y
需要创建设备别名原因:
& k9 H( |' y6 ?4 z在运行一个ros程序的时候需要提供一个端口名,这个端口名一般是ttyUSBx,设备每次插拔对应的这个端口名它都会不一样,需要创建一个设备别名,就是要将这个端口名来给它固定住。
5 v% {" b; s8 y
- `1 l7 S" H9 y6 E8 X a- P, r& L
重新插拔 USB1端口的设备后,变为:
$ ?& L8 ]3 b# p+ r5 [4 @8 Y3 D
/ k: s: y: |5 I E2 t
$ D( d7 r) f6 T( l' M: ]
可以看到变成了/dev/ttyUSB2 . I' L& _* K6 g% E; M
端口号发生了变化 % d `( D) @2 T& e7 V4 C
创建设备别名需要写一个脚本文件,如下: - 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& e6 n: A3 a/ i7 r$ L; c
- 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.rules5 h, N; l Q- l) [/ z, t
0 s9 X1 J' j- o1 y: n1 ^+ M- service udev reload
4 Y' h# a3 a% t$ z) M - sleep 20 W1 S& `; ^; m, P8 j! [: P$ T
- service udev restart
复制代码
1 G% D3 L2 S! y: M% O
解释下上面的代码 4 t* ^" p0 `% D5 P% V
KERNEL==”ttyUSB* 不管是USB几的设备都进行判断
0 ~' y% w- s/ V1 f6 k5 v5 {7 fATTRS{idVendor}==”10c4” 这里的idVendor ,在前面win上修改 serial的时候出现过,就是10c4
! p1 ?3 g4 ~3 ~8 P+ g8 |
1 t9 |4 C; C! P. n6 [
2 @; L' v6 [5 i0 }4 i6 I
ATTRS{idProduct}==”ea60” 这里的idProduct,在前面win上修改 serial的时候出现过,就是ea60 3 S9 t& s- j2 I0 e
: `' b4 B2 o* m! Y9 E' ^) U
6 E& B" F3 }& G5 Q. n4 x6 y
ATTRS{serial}==”0002” 这里的serial,就是前面我们改过的,这里就根据这个值的不同,定义不同的设备别名 * j& ]. v6 T% K" e1 S; \5 g
MODE:=”0777” 就是端口的权限
/ M. S" Y/ P, C) h2 ]SYMLINK+=”stm32_controller” 这里的stm32_controller,就是取的设备别名。
, h4 q! I0 d" \, z' ] I! f! E
所有上面第一行代码的功能就是,将满足这些条件的端口的设备名称改为定义的设备别名。
& J3 }; M1 F3 |! d0 M
脚本的文件名称,取名为change_udev.sh。在执行前需要给这个脚本文件赋予权限。 - sudo chmod 777 change_udev.sh
复制代码
: E) G. j J. x; J" D, ?
赋予权限后,再运行这个脚本
/ H, a) a' T1 W( t l
这样就运行了设备别名的设置,之后不管怎么插拔这两个USB设备,系统都将会自动的将这两个设备去给它赋予设置的设备别名。 7 [9 r) t$ l) G$ I3 p- U
使用设备别名
. H+ i! k0 y0 l5 i" W( _: t$ s0 |
在上面,设置了设备别名,下面来看如何使用我们的设备别名。 3 e: u5 c; d$ R2 p) m
例如我们将雷达的USB的设备别名改为了2d_lidar 4 F! [: y( w' f; z- z
雷达的roslaunch启动文件则可以写成如下: ) m# K7 `$ }3 Y
- <launch>' _, h7 k K. Y7 U$ Y2 B) ~. m9 X
- <node name="rplidarNode" pkg="rplidar_ros" type="rplidarNode" output="screen">; X3 S, p% B2 d& T9 S
- <param name="serial_port" type="string" value="/dev/2d_lidar"/>, p9 }1 S; v4 ~" K; k/ l
- <param name="serial_baudrate" type="int" value="115200"/><!--A1/A2 -->
* e+ k& `# A2 e3 O - <!-- <param name="serial_baudrate" type="int" value="256000"/> --><!--A3 -->
4 z( Y/ h, J- X# j) n - <!-- <param name="serial_baudrate" type="int" value="1000000"/> --><!--S2 -->
' T7 _8 w$ _9 v2 d9 i - <param name="frame_id" type="string" value="laser"/>" j, j( M2 d& I: y2 I4 {. E
- <param name="inverted" type="bool" value="false"/>
% A x2 S# S# t$ d' O - <param name="angle_compensate" type="bool" value="true"/> 7 ^$ t* W# E! |5 |: M# [" k
- </node>! @7 d& M; t( u& r& P, |
- </launch>
复制代码
2 U' s) Y# f. n k" l: _& y+ z1 S
上面的代码中, - < param name=”serial_port” type=”string” value=”/dev/2d_lidar”/ >
复制代码
6 S% z( d8 {& y- B+ _; c! i
这里,我们就将系统的设备别名/dev/2d_lidar,设置到了参数serial_port中 5 X0 w+ Q3 `3 p
" l% c% k. z/ L( ^6 |& I
ROS与STM32串口通信代码 ! |% K$ w: `; l/ F A
这里以一个智能车代码工程为例,抽取串口通信部分代码
0 R2 d6 U2 s- u
在头文件中,进行串口头文件的包含 - #include <serial/serial.h>
复制代码: E; O4 P& [6 R% J& p8 X
在类的定义中,什么一个 serial 类的实例 - serial::Serial Stm32_Serial; //声明串口对象
复制代码
6 J; J. q6 ?# y ]' p4 n
并且在类的定义中,声明两个结构体,用来存储接收和要发送的数据 - RECEIVE_DATA Receive_Data; //The serial port receives the data structure //串口接收数据结构体
- c8 n( ]7 V/ J* [3 e' P - SEND_DATA Send_Data; //The serial port sends the data structure //串口发送数据结构体
复制代码7 P4 `5 H$ o) B& u! _
在类的构造函数中,配置这个串口对象的参数 - private_nh.param<std::string>("usart_port_name", usart_port_name, "/dev/stm32_controller"); //Fixed serial port number //固定串口号
/ H) S4 v1 v. I - private_nh.param<int> ("serial_baud_rate", serial_baud_rate, 115200); //Communicate baud rate 115200 to the lower machine //和下位机通信波特率115200
复制代码
+ v% R/ ]+ l+ v' K, ^
这两个参数是在launch文件中设置的,代码里进行参数的读取。
# `4 I0 j/ |% {/ w& X/ Dusart_port_name 设置的USB设备别名
% O: g5 ~0 x/ \0 S' R# Qserial_baud_rate 串口通信的波特率要和stm32设置的一致
& c4 @ r1 Q G
- try
7 q; h5 x6 f8 n: n8 z2 i - { ! W: u2 A3 Q$ `: a* r
- //Attempts to initialize and open the serial port //尝试初始化与开启串口; Z* ]0 _- L6 R' q3 ]
- Stm32_Serial.setPort(usart_port_name); //Select the serial port number to enable //选择要开启的串口号* T8 {9 o4 y0 L0 @) Q
- Stm32_Serial.setBaudrate(serial_baud_rate); //Set the baud rate //设置波特率
$ l) {% O' e! i5 ~ - serial::Timeout _time = serial::Timeout::simpleTimeout(2000); //Timeout //超时等待+ K4 c9 a# i+ f8 J4 `+ I$ O
- Stm32_Serial.setTimeout(_time);
" ]/ |) r6 S1 z - Stm32_Serial.open(); //Open the serial port //开启串口7 x2 h0 K# ^+ P' t. u, l8 S
- }" U7 O( k9 d! R' o
- catch (serial::IOException& e)6 G& L1 C- G, P$ ]' _( z R
- {" D6 h' `2 `5 N9 n$ E- F
- 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 //如果开启串口失败,打印错误信息+ z& ]( |0 g4 v/ ?- I! A
- $ ~7 | y4 w1 y% v: I7 W
复制代码
( \) v+ C9 k3 Z
初始化串口配置,并开启串口 ( I. I- z) h. |. C0 _
设置的参数包括: 要开启的串口号 设置波特率 超时等待 " R8 Z& X" l; }% n
8 H+ ~. M. E. y5 x. v
判断串口是否被打开,打开输出终端打印信息 - if(Stm32_Serial.isOpen())
! o, Q2 O8 A# c; `2 {. W. `4 n0 [* Q - {
7 V" D: H- Y1 t1 t, x7 W' t9 v7 Q7 I- } - ROS_INFO_STREAM("car_robot serial port opened"); //Serial port opened successfully //串口开启成功提示
5 b8 p' U2 v# ?: G- B - }
复制代码
2 g8 z0 j! {' y, \+ _6 {( E. c& n
ROS主控读取stm32发送的数据 $ W- Y" D+ P. u+ H, ^1 g! Y
之后便可以通过 - Stm32_Serial.read(Receive_Data_Pr,sizeof(Receive_Data_Pr));
复制代码
# \) U" \/ I9 {- |
read函数读取串口接收到的字节,之后通过定义的通信协议再进行和校验与数据解析即可stm32向ROS主控发送数据。
$ p* d8 L1 l( A
ROS主控向stm32发送数据
# J0 w. O: T0 H
ROS主控向stm32发送数据的代码如下: % L6 {6 J: W. ^" i
将之前定义的发送数据的结构体 Send_Data的tx 中填入要发送的字节 - Send_Data.tx[0]=FRAME_HEADER; //frame head 0x7B //帧头0X7B" e; L; g8 s# \, ^( b4 _0 [
- Send_Data.tx[1] = 0; //set aside //预留位
; I' E/ V* f% ?' y' w) |- w - Send_Data.tx[2] = 0; //set aside //预留位
复制代码
& W: j+ `: m( U9 @5 p, c/ h
填好字节后,直接通过下面代码发送即可
* R1 x% j7 u* i% S! O- try
/ g1 u) M% L# n1 a3 h- T - {0 [, U6 v6 k5 q/ i* T
- Stm32_Serial.write(Send_Data.tx,sizeof (Send_Data.tx)); //Sends data to the downloader via serial port //通过串口向下位机发送数据
# u- L5 S3 ~1 a - }9 g: e' o2 R7 ], O/ W8 b, G
- catch (serial::IOException& e)
+ ~- c* \5 S9 R/ l7 Y( b" H - {
+ _- Y" [& m" T E# {$ p1 L - ROS_ERROR_STREAM("Unable to send data through serial port"); //If sending data fails, an error message is printed //如果发送数据失败,打印错误信息# I5 B- @/ {7 z" X
- }
复制代码 |