通常的机械臂都是由多路舵机组成,我使用的是某宝上(并不)常见的五自由度机械臂。尽管商家称它为六自由度。( U3 {. ^( U' P8 F; ~5 ^
这里使用STM32F407VGT6的6路PWM输出通道来控制6个舵机的运动,树莓派(上位机)通过USB转TTL模块与STM32进行串口通讯 + S- u2 w" {: P! G* }0 ]* K
PWM舵机控制原理 p- t6 o3 ]2 c6 U3 u
: C9 W8 \ ^+ r: v6 U2 W' X7 `
标准的 PWM 舵机有三条控制线,分别为:电源、地及信号线。
9 Q4 c+ N8 O5 r: \$ ^; S( c- f7 c0 ?2 v1 H
- s0 H" M: b( S( l
市面上大多数180°舵机需要的PWM波周期通常为20ms,高电平接收时间通常为0.5 ~ 2.5ms,对应舵机旋转角度为0 ~ 180°。用PWM波控制舵机时,只需将时钟的周期设置为20ms(50Hz),并且通过改变比较值pulse改变PWM波的高电平时间来控制舵机旋转的角度.
$ {! R( I. N( L% E" w8 U2 m STM32CubeMx主要配置8 L1 H' @& ]4 l! N' j( L, l
对于STM32,我使用的是STM32CubeIDE + Mx 进行开发。时钟框图为默认配置,如图。
1 `2 n5 d$ W/ Q5 @1 r7 z) z, j
% m' y$ ]# H0 Q: V; E& p4 U
. r; C7 e( n( ^% J( C TIMER& l" N+ v1 X: |; X; S, @
由于需要控制6个舵机,我选择了TIM3(4路PWM输出)和TIM9(2路PWM输出)。2 H g# S( J) _ z
PWM输出的频率由时钟APB2决定,由时钟框图可知此处的APB2频率为16MHz;PWM波频率计算公式为: W = APB2 / (PSC + 1)(ARR + 1) 其中PSC为分频系数,ARR为自动重装载值。这里我将PSE设置为39,ARR设置为7999。
1 m! g9 t5 }4 O2 \6 c
& g* R4 H$ L0 F1 }( Q% N8 p4 d1 b' ^1 x$ q
注意,这里的计数模式(Counter Mode)不同时,会导致接下来比较值值相同时舵机的旋转方向不同。
由于PWM波高电平时间需控制在0.5 ~ 2.5ms内,所以各PWM通道的比较值(pulse)必须控制在200 ~ 1000之内; (8000 x 0.5 / 20 = 200)(8000 x 2.5 / 20 = 1000) 当舵机旋转角度为90°时,pulse值应为600。这里我将各个PWM输出通道的比较值均设置为600。
! \( s- c# j! v/ i% Z
- d6 r3 W( h; E/ c/ C由于我手头上的机械臂中的一个舵机用于控制机械爪,其为90°舵机,所以在配置控制该舵机的PWM通道时,比较值范围仅能为200 ~ 600,中间值为400我使用的单片机为STM32F407VGT6,其TIM3和TIM9所对应的PWM输出通道分别为PA6, PA7, PB0, PB1和PE5, PE6。
* p9 g/ I$ |2 u5 u6 ~8 J8 K/ g这里只需将这些IO口设置为复用推挽输出以及上拉即可。
`+ h" J; O: a; T0 D: i6 i
! R7 G7 M! r9 o. ?) P% r! | y2 d7 j3 r/ U1 _* T* c
串口配置
2 c' P5 n# F1 L5 y9 m+ r# j这里我选择USART_2用作串口通讯,其Tx与Rx分别对应为PA2、PA3。 9 ?4 a1 t# ]" D# ?6 H. y1 g
串口的配置如图所示。 / w) C# m( {8 {$ U3 b" q
( j- A5 }) q H3 k2 U% |! {/ n& E: l2 }
这里使用异步通信模式,注意波特率等参数需与上位机相匹配。6 ^1 T* L+ T3 x4 c0 G6 l! T; ]
! N h+ a% ?: H; f
6 I$ t B: U, H; A2 ?8 A3 P
PA2与PA3均设置为复用推挽输出,上拉。
" v" R; P& L# l. @% g( ^ 中断控制+ k2 M; A6 I' q* J5 Y
由于这里我只使用了串口接收中断,所以NVIC这里可以不用配置;若STM32除了控制机械臂外还有其余任务,则可能需要配置NVIC。 3 A) b4 h1 i5 o* k/ ~9 @1 o
STM32CubeIDE代码实现- N% ~/ ~7 [ J7 |3 M0 s) q
在STM32CubuMx配置完毕并生成工程之后,我们的主要任务只有设计上位机与单片机的通讯协议,以及完成串口接收中断函数即可。 9 I+ P6 `2 ^" H2 f4 G1 v
通讯协议设计
7 c3 F* F2 g# T: x我初步的设计为串口发送一次数据,控制单个舵机的角度;所以发送的数据中需要包括舵机的编号以及舵机的目标角度(或者由角度转换而成的比较值)。 在这里我没有设计通讯头以及对舵机转速的控制,因为这里我的串口只用于传输控制舵机的指令,以及我使用的是小型舵机,其转速并不是很快,可以满足我的要求,无需控制其速度。 设计思路如下:6 n) j3 M& P* k- k
上位机每次发送16位的数据(2个uint8_t类型,串口只能发送串口),舵机编号为0~5,发送的pulse值为 (200 ~ 1000) - 200,将舵机编号数值乘以1000加上pulse值减去200得到一个uint16_t类型的数值;再将其转换为十六进制进行发送。 例:对3号舵机进行操作,角度为45°。 数据处理: O6 a6 [5 N. Y8 _
3 x 1000 + (45° / 180°) x 800 = 3200;0 F2 n( D" C5 J+ T9 k ?& B, }' A, p
3200转换为十六进制为:0x0C80。 STM32进行逆向操作得到原数据。 6 m( h% J* U% @
STM32代码实现由于串口发送的是两个uint8_t类型的数据,所以在解码之前还需将两个uint8_t类型数据转换为一个uint16_t类型的数据。 在打开USART2的接收中断时,设置为接受到两个uint8_t类型的数据进入接收中断,以便于在接收中断处理函数中进行数据处理。 / Y: d) ~$ T# T& N3 g
- uint8_t pulse[2];6 X9 W; r/ |/ @2 H
- HAL_UART_Receive_IT(&huart2, pulse, sizeof(pulse));
复制代码
: g6 f( e$ u1 ?) B4 w7 g# j
9 g$ w4 w3 C( v8 n2 p
串口接受中断函数代码如下: % V; D j8 {- D0 l" k
- void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)" N6 T) ~4 _) n9 S, i/ I, Y6 w
- {
. L) K- O# A$ W0 }5 ^ - if(huart->Instance == USART2)0 x- E. t* J; L) _" d& |
- {
. R7 X a9 A4 T; T2 \3 C - uint8_t rec0, rec1;
. g1 j( k1 p. J4 v! n) ~4 V
. @0 P- |& k+ g3 o* ~5 T- // 获取接收到的两个字节的数据: {7 `7 X4 T9 R# r0 K- ]
- rec0 = *((huart->pRxBuffPtr) - 2);
7 v* M$ N- O" `3 X' f. \ - rec1 = *((huart->pRxBuffPtr) - 1);6 C# p2 a R4 j- O5 L1 K4 k1 T3 j' [
- 2 R4 B7 l' l! L% @; L0 w9 K) @
- uint16_t numPart[2], armControl, arm targetPulse;$ \2 [" O! A" x& a. H5 q
- 1 A' b' ^% k* S, a: |# J7 S
- // 通过移位与强制转换得到uint16_t类型数据
/ n# u; [* J8 \, B8 @* w9 ~, d - numPart[0] = (uint16_t) rec0;2 ]# V/ r% B4 z/ o
- numPart[1] = (uint16_t) rec1;6 ~- m5 N3 ]) v% ^
- armControl = ((numPart[0] << 8) | numPart[1]);3 R8 J5 K6 _6 x% w' q
- 9 S% y+ |9 i5 E& K6 H
- // 逆向解码' g c+ D: X) `9 ~* y/ Z" g6 a2 g
- arm = armControl / 1000; // 舵机编号
~" `7 r8 D: Q' X8 A3 ~ - targetPulse = armControl % 1000 + 200; // 目标比较值+ m. s! t' O& Q- a$ i
9 {1 }' B1 u5 v- // 通过switch语句改变指定PWM通道的比较值
- y8 l1 B8 @5 [ - switch(arm)- F4 d- J: m6 G7 p3 R ~) f7 N
- {
7 B2 j) j4 M; X8 m - case 1:
8 B K3 O3 a- p& b - __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, targetPulse);( v4 {+ ^: K: F
- break;
. ?8 `: u* X) Z% T$ {! f
( B' B9 f2 ^6 {- b1 g9 `1 F+ W8 l- case 2:
, ?5 v# u" L8 _8 ] - __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, targetPulse);8 ]3 b+ c7 R% ?4 N: @' ~( \9 Z
- break;
' n; K8 v7 |8 E$ d/ {
3 l; V$ A7 F( t- case 3:
, U" p( t1 [; \' ^* u - __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, targetPulse);
; t$ i! s# p2 F4 b# I4 F& m8 { - break;
- j( U+ W+ l2 @, z
7 H Z0 s9 ^4 e- case 4:
4 e4 I+ E2 O5 m - __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, targetPulse);
* i' \! _3 [$ j4 q( K - break;
1 I ~0 T2 A" ?8 P
; M5 _% W0 A/ q$ t$ Y, F8 f- case 5:9 z4 Q7 D1 J5 {; |. X) L0 k0 e
- __HAL_TIM_SET_COMPARE(&htim9, TIM_CHANNEL_1, targetPulse);
$ Y& A, E9 |; _+ W - break;; v$ U, q; i, d5 U+ N
- ! L- w7 f) @1 T$ d7 F1 L3 z4 q6 a
- case 6:3 i' p. ~7 f( r
- __HAL_TIM_SET_COMPARE(&htim9, TIM_CHANNEL_2, targetPulse);
1 a7 y( f& W( M& F1 c - break;: T$ C! ~# b' n1 u4 a! f
- }1 j) V' d/ S/ B0 }
- }
. ^1 {3 B$ ?3 ?. C+ N8 w - }
, L2 k2 \) S) }+ T6 S
复制代码 8 u- N: \) G6 n; q A: E8 `; g
在进入while循环之前还需将PWM通道使能;
8 Q& Y2 n2 i7 Z( h- HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
N* q7 E& [) J+ Q" S/ \+ X - HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
% o! ~, P7 n& R7 i& @ - HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
1 j( T1 ~. E2 O, [! n - HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);: b. k2 X9 A8 ~/ _& |$ F
- HAL_TIM_PWM_Start(&htim9, TIM_CHANNEL_1);
7 m3 I4 u6 R3 n! V$ P* ^! X - HAL_TIM_PWM_Start(&htim9, TIM_CHANNEL_2);
复制代码
* J7 j6 n/ Q9 {
, l, q) o- c. n( l4 V8 P& ?
并且在USART2_IRQHandler函数中使能串口接收中断: 4 ]" S' f( K1 v* n1 z
- void USART2_IRQHandler(void)
5 A% \% M) }. `( F$ P6 V7 `4 v - {
- k" S+ Y0 u7 _7 \ - /* USER CODE BEGIN USART2_IRQn 0 */
8 u/ x6 ^8 D+ P$ I0 ]) e
1 k; Z7 ?% E: F9 Q1 O* ^- /* USER CODE END USART2_IRQn 0 */
& d: ~+ c- v0 _( V. b: u0 o - HAL_UART_IRQHandler(&huart2);# M7 `# N, a% _; D* w- q7 Q) G2 |
- /* USER CODE BEGIN USART2_IRQn 1 */
1 g* v# R H6 G3 A8 D - HAL_UART_Receive_IT(&huart2, pulse, sizeof(pulse));2 w$ Q0 i, X3 Z& Q, W7 j# o: u
- /* USER CODE END USART2_IRQn 1 */* M* g* ^ y3 z; m2 o5 W c% y
- }
9 l$ G; ?4 k. g @
复制代码 4 r) J+ A" a* I6 e
5 }6 u5 t7 v6 X测试! j6 D8 n t( K } e
在测试时我使用的环境是安装有Ubuntu 18.04的树莓派,串口调试工具为CuteCom;由5V锂电池为单片机与舵机(机械臂)供电,每次手动发送两个字节的数据,十进制转十六进制在科学计算器上进行。机械臂的运动符合预期。 ) e( f& x) o* n: f* t: ], V* F
经过测试,同时发送12个字节的数据,机械臂也能够正常运行。 , p, n7 D M% `& T" K) ~
这里需要注意,在接线时,舵机的地线必须与单片机共地。 5 r( @1 ~' I$ c. Y% P+ f7 C6 A
之后就可以将上位机串口发送的代码写入C++或者Python程序,通过上位机控制机械臂。 + o! R3 F9 o+ ]/ \2 C
# b& e: C, w0 B( e. L3 d- { L* ]9 B
|