基于STM32的编码器详解, @, X3 `) m6 w9 p
基本原理:
' p8 _7 A# J7 J K* \$ R S 光电编码器由三个部分组成:发光二极管,码盘以及码盘背面的光传感器。: O2 I( S" x- s
这个码盘安装在旋转轴上,上面均匀地排列着透光和不透光的扇形区域。当码盘转动时,不透光的部分能够挡住光线,而透光区则允许光线透过,那么码盘背面的光传感器就会周期性地收到光信号,从而输出一列方波。0 L3 Q. B. f/ U3 ~8 P& s: S( @, A$ n
% H5 b6 z5 g* d; K( Q8 J* @: Q! }
我们知道,码盘转动一周时,光传感器输出的脉冲个数是一定的,通过检测一定时间内收到的脉冲个数,就可以知道在这段时间内码盘转动了多少圈,进而换算为速度。例如,一个码盘转动一周时会输出100个脉冲,在0.1s内我们收到了500个脉冲,这意味着0.1s内码盘转动了5周,即码盘的转速为50r/s。
/ @* v# [% p) x- V+ ^1 t' } y& K3 K2 \/ W& r; x- x2 ] p
; A8 |2 _7 k( m: k3 | 但是,还有一个问题。设想,如果编码器只输出一列方波(假设为A),我们该怎样判断码盘是正转还是反转?因为无论是正转还是反转,都会产生同样的方波,而它们对速度的贡献显然是相反的!
! |& g% B! C. P! Y0 N6 z: \( m- }* _1 D/ p( s3 K+ Q ?+ f9 _+ q
9 }+ k' v+ e0 }" G1 o& i" _" Q' E
接下来我们看一看这个问题该怎样解决:
' y) w4 a/ K3 t) L% Q; f8 a& c* o; C( }" s
- {7 ?/ R1 j, t 上面我们已经说过,码盘上均匀地刻着透光和不透光的扇形区域,我们在这一圈扇形区域内再均匀地刻上一圈透光和不透光的扇形区域,不同的是,外圈和内圈的区域是“交错”的。也就是说,当外圈处于不透光区域时,内圈对应的一半为透光区域,一半为不透光区域;当外圈处于透光区域时,内圈对应的一半为不透光区域,一半为透光区域。 @! J& {: T5 p
0 H& G9 g' s1 l. B3 l+ `1 b' _% k( I( i- M m: ^- F; ?6 s' w+ [ q/ Q
于是,当码盘转动时编码器会输出两列相位差为90°方波,波形如下:# _& y5 X0 l0 X
; t/ z9 F0 N; k9 ~
- H" L( z, Q/ Z$ ?! ]; l8 y
/ e% D9 x1 W: v
/ R( s% A7 a F0 w+ v1 o) ]% Z
/ k7 _0 O* c: E
下面左图为正转时的波形,右图为反转时的波形:' G8 {( L& ~: \' u# k2 x+ l/ C& }
7 Z* ~4 t2 o0 @" d, ^+ i- T! N& S& p# Y9 I$ Y9 W: z
0 `! n+ q6 k8 o) g& g& n
; n' v! g! L9 x
$ W4 s5 i6 \+ l& t' y g7 m
当码盘正转时,在方波A的上升沿,方波B恒为低电平;当码盘反转时,在方波A的上升沿,方波B恒为高电平。通过判断A处于上升沿时,B的电平状态就可以方便地知道码盘旋转的方向。: z3 X# K, P- T" b* s- O
* Q$ U6 R! _- N! S8 p6 \: i$ q
: { t. P* H7 d' l; S 速度计算:
6 m0 M; y a% J: h- I! _8 W- B; [0 L6 T4 `1 B$ Z/ N
_5 I# J3 Y# z( F- D! b 不同型号的编码器,码盘旋转一周输出的脉冲个数是不同的,我使用的编码器码盘旋转一周输出的脉冲个数为90,小车轮子的直径为75mm 。
$ E+ [* L1 P% H) B! @4 k' M9 _. Z* s
& S F: s ]) X4 H
假如我们在时间T内统计到的有效脉冲数目为S(正转脉冲数+1,反转脉冲数-1),小车轮子的直径为D,圆周率为pi(约为3.14),那么小车的速度换算公式如下:
. ~) k/ Z y4 E
( N3 P9 D& U- `% h
- I9 F" R, x* ^% q V = pi*S*D / (90*T)
- \, @9 \, n+ _8 a) z0 {4 l0 ?. k) W7 D% {/ m& [
' k2 [7 @7 v' C8 F' x' m- K
例如,我们每隔1 s统计一次有效脉冲数目,某一次得到的有效脉冲数目为500,那么小车此时的速度为:, n4 ^3 M* F3 d9 V. O% v
: Y( [% {7 d2 G1 @& ~
# D1 {& _: m) J1 HV = 3.14*500*75*10^(-3)/(90*1)
# j% m8 f. b& v- \7 m
* ?% D. g' ~+ j0 t+ z, C( V. k1 N% `9 P5 z' H
≈ 1.31 m/s. F% \6 s- g3 M
1 x! a! o9 e( j9 ?! ^9 U2 g
* s9 v0 U& z4 h2 ` 我们可以看出,问题的关键在于,如何统计一定时间内的脉冲数目?目前有两种思路:" Q5 e p" {$ h' c$ P) [
+ u% z: Z# M! J/ u0 Q# S5 z" U- @, U7 e0 ^, a% E
①利用中断检测A的上升沿,触发中断时判断B的电平,来决定计数值加还是减;. x( _6 s; a6 q) j& W7 z3 |
7 ^" f9 j3 Y5 ^5 m, p& G) s. ]5 E$ z* ], |; e5 x. a6 `9 j9 v1 T$ ~# T
②将定时器设置为编码器模式,直接读取计数值和方向。
% r: I8 _1 T5 f3 \6 k, J+ C$ t1 Y
W7 V5 J( X& {% u0 ?2 N9 ~9 l6 G9 y J% f4 J) C5 b4 c) `1 u
下面分别介绍这两种方法的实现:+ E0 G2 d5 B. p% M6 c! {3 j
0 f- {! Q$ t) D9 F7 {; a7 f( Y( ?# g* V0 j9 X
代码实现7 Z" v4 V9 K' b- U; i8 I O2 x
利用中断检测A的上升沿,触发中断时判断B的电平,来决定计数值加还是减
1 I% N1 `) X0 R) `9 Y; y$ h
7 j/ v$ ` t7 w7 v( J" A6 E8 U% o0 F- q/ l
(1)打开CubeMX,设置相关管脚,我这里使用的是PC2和PC3来 接A、B相
4 O1 s( I/ q1 i _. j, [
9 I8 u' n% X+ w$ H
- z* @) E( _1 [7 f% s9 y
Y" F+ Y: m% u
0 q/ k! O; ~1 Q3 z; [8 t, U9 j
3 K, N/ O& ~7 I4 D+ r5 }, f设置A、B相接口
! W8 M0 T* H; ]8 a& T5 s4 t- N+ @: B
: ^3 A- A3 J* m! k
3 @& b9 O! z7 Q* Z! O* x0 b
' b, ]% X5 ^3 O2 F' L4 T% P3 c
6 x9 |; t2 {4 A3 ~- d. h# x {3 M5 e7 l" k8 c- i3 _
将PC2设置为上升沿触发; L+ p3 Q" G: Q: m- {
" n- y; d% H( l
- W- g* s4 y. t& z, u/ D) M! c(2)为方便观察,打开UART串口,从串口读取采集到的数据8 N" V% X* q/ C$ F
L" R3 \$ g3 ]
& h0 j" T7 v# R! D1 X# {7 k- z
0 m) H8 c& w& C$ b8 Y* c1 o, r2 D p P N
" F4 g, o' G2 g2 b0 R' c打开UART1
% h1 }- N4 U/ s+ m' }, Z" _
6 X2 i/ |% c: E4 s2 ~' A+ q% J
3 q/ ^! v/ Y' ~
+ O2 O5 E) q: f/ l }
2 q+ }; W' m! g' f b3 t4 F# C- N4 `/ C# F6 I" C# G
UART串口设置
( s. S6 v% J: {/ m
5 f: x; x. ?8 v5 c& V% n* c6 k- A ?9 l' O0 ]
(3)打开定时器2,定时器响应时通过UART串口把计数值打印出来6 a+ W9 v& S% _+ c* z% n
3 x( |7 \9 ~) B$ W6 F( `
2 l' N6 `) _2 L0 x7 b1 s7 `
; g% a, k) H) w
- W( d. E9 X4 h, P/ Z! U" D
1 ]! @8 i& M2 N
配置TIM2
- i2 U- y% G4 _. o8 u8 G P9 ?3 p
4 A' l9 k) r$ G( `6 p& u% F
; j/ [0 O, t: E& Z! }9 i! @. {
1 B, {0 ]& q$ U+ w
7 |3 k% R$ y8 w* Z& ?
计时200ms1 b/ Q/ P2 Z) K6 d
6 v8 O' W( L# F8 r* R
* U# @" S- s& Y( O& j; E5 k( t(4)导入Cube工程
/ y9 T! s' y4 _ h/ |9 t- 7 u/ f* _8 O: z) D9 h% ?9 ?3 x
- int COUNT = 0; //用来计数2 Z& _) y3 e- x# K4 t0 U( x& ~
- char str_buff[64];% f# Q. X1 a2 M) X8 i7 W
- char point[64];
) s7 `$ Q2 Y7 O: k+ X' @% b1 s# v w& O - float speed = 0; //速度 D$ |& J% c9 t6 ~) G
- int flag = 0; //flag=0时计数,flag=1时清零
复制代码 定义相关变量* v$ |0 t/ j( I
* H0 e9 x2 Q6 b$ \- void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)' g, G. X1 B9 ?
- {
7 I9 \+ | T6 a& Q4 D4 f* x - if (GPIO_Pin == GPIO_PIN_2)
K% D+ m6 S! v8 {0 e - {& {6 y: o, H' b1 x, q- T
- if (flag == 1)( a* h$ g# Z- T! Q' e+ {7 `1 i
- COUNT = 0;
) ~- ^6 z$ q0 f2 i' R - else- A+ D; B& S1 ^; q2 T* _5 a
- {3 L0 V7 I' o# y; h1 N$ N! X
- if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_3) == 0)
! k7 [$ D8 h9 f, _/ I5 T - COUNT++;
7 ~" `0 H1 t1 I F" X - else3 z9 j: d" F/ x& C' \- ~: w2 r8 t, {3 U
- {9 x7 N; ] G+ A5 E# S2 C
- COUNT--;/ T$ l6 @" O* o
- }
. g. _5 F. r, }5 {# G - }
/ i7 V. m0 l. f& ^ - }
- b6 M5 p: y0 Z3 c: { - }
复制代码 重写中断回调函数
! J, g* T+ l c- , W* Z& v6 W4 o3 E X$ `* p9 P% V
- void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim), p' z( U4 Y" N( P2 x/ A: T7 G5 _( `
- {$ P w, g4 c1 Q, }$ ~4 P
- if (htim->Instance == TIM2)
H* M) U8 x& N& Q5 | - {
5 P- g3 K, s# m. n K - if (flag == 0)
( ~: o9 l" h. R1 n7 k! {) A+ F - {1 }# ~: q5 a- [+ N# d
- speed = (float)COUNT * 3.14 * 0.14 / (90 * 0.2)*100/34; //cm/s; V+ B+ O+ r# X1 @8 ?; A
- sprintf(str_buff, "speed:%dcm/s\r\n",(int)speed);
+ y" C8 H% Y$ {& k - HAL_UART_Transmit_IT(&huart1, (uint8_t*)str_buff, sizeof(str_buff));
9 A; g; Q ?+ e; I# \ - flag = 1;
9 ]. D5 q q9 r% ?, ]) R% p" i5 b0 C - }
% U- c) x4 p/ O - else9 I$ d7 m4 {- ^' j$ z: y1 s* w
- {" C4 N; L3 K( u
- flag = 0;0 ^# ]5 ?. q( H8 A n. ~) m
- }
* A8 k' l+ F, _/ t& u* X2 N# y - }
7 t6 }9 K* T! B e# P - }
复制代码 定时器响应后通过UART串口发送数据) B; \8 W* L u; [# t/ g
4 Z. L: o/ H4 u! N1 d- HAL_TIM_Base_Start_IT(&htim2);
复制代码 在主函数中打开定时器2. T5 w5 ^+ e6 ^, v" o
* t4 r. r' f1 i9 n
* N/ X6 N; @7 M
实际测试 R3 t9 a& Q# f' U' K B
测试效果如图:( b0 W* ]' O- A2 S1 X
/ Y+ X2 F" q% |) O8 Z# @! C* Q. @/ L+ `5 g( n" [
8 ^- l9 w4 k) b/ d- p7 n" |" `
# E1 ]1 ~ ], e/ ?1 D' L8 f" `8 A实际效果4 k) r; Q- _7 {4 N: ]2 e
a1 f& a1 J L2 }! N2 n; @* x+ _
9 x2 Q/ L- u5 V2. 使用定时器的编码器模式
* V3 [4 b8 p; F1 i! `: y6 u. a( ?/ x4 z$ f! n# M8 w
! f: M r0 ?. G. K(1)设置TIM4为编码器模式
0 w$ [- K1 r% M( n6 Q, C
& T1 E8 G$ A( z4 {( q3 _5 `) J/ b+ \% S8 r- m6 ~! [6 [8 n7 q. K
9 c) { U$ T5 G) n- A5 N- a, K
: [" x% D- h, s! j8 _7 a2 t1 q
3 a! z9 a9 w W3 Q5 k+ L设置Combined Channels" s9 e% w% t, w @# u( Q$ a! C4 O
2 i- O( B. T$ o" G) T1 C* N0 e# o* ?" J8 T+ l
(2)配置编码模式
P- N9 P! k- k
1 m, h- `6 H; p- f' f4 M
. N X& q$ O7 J; }) z" F
: d, Z3 v+ R, v# }/ Z$ dPrescaler:分频系数 J- h H3 Q7 {; E1 l8 [
3 @% E# G$ q; ~$ y( k1 @2 u: L: U
+ h1 M3 v% _( ^; ~( l, _Counter Mode:计数模式,设置为UP代表码盘正转时计数值增加,反 转时计数值减小
+ Y5 f: L: j1 v/ Y- o# W0 K4 W L8 H: ]1 s7 D7 C8 O9 O9 R6 h
9 {: y. }; s5 [# r$ M' X
Counter Period:当编码器计数值等于该值时,计数值会清零,一般避 免编码器清零设置为65535(最大值)
8 W# [! {: w$ Q/ L" g4 d$ N% V$ Q7 B
% m/ ]$ D( k5 r: c. t PEncoder Mode:计数模式,编码器计数有三种模式可选: r! B, b9 P+ m9 B* K j
2 M m" l3 M' r7 W. ^5 N
: c$ R# G* L$ @
①T1. }. b% H! ^0 P$ ~. e
) _: e p% H# W8 l+ X' h
* X& j c5 J+ h8 A$ ]" u' g只在上升沿计数,例如在一定时间内A产生了100个脉冲,那么编码器计数值为200(A、B产生脉冲数相等),由于分频系数的存在,实际调用函数得到的计数值为200 / ( Prescaler + 1 );
8 S5 N+ m0 O3 }4 ~8 X
; }! l! W: n* [" ?* [2 s3 K4 z2 i) G8 k2 Q
②T2
) R0 S7 m* R% P. o7 D! i g# j4 I' c, G* {# D
+ o; c2 t* k! a' ?8 u! x: Z1 i. `只在下降沿计数,计数值与T1相等( Prescaler相等的情况下);. _# [- q0 B. _. W
5 [1 b; k- w& Y, v
( J1 `' N: N9 i/ F N- }1 v③TI and T2
. D2 F* J* M+ N. U: q9 ^
& V/ F: Z0 }2 |, L1 ?- }
) q( A( W$ x0 Z) o/ W7 [. \在上升沿、下降沿都计时,计数值为T1的两倍( Prescaler相等的情况下);
5 [+ L% x+ e" N
) z7 @* X9 G2 H& ^' R( x& x0 S3 k% \4 E# _+ x) y6 e
(3)导入CubeMx工程
# y3 c' b4 e# c5 h( v- 3 s4 a0 R% P" q
- uint8_t GetData; //编码器计数
& e2 N, F9 x8 o; f - uint8_t data[64];
# l# r: `% r2 u; \ F9 f; ? - uint8_t Direction; //编码器方向
复制代码 定义相关变量 @' D) |! y+ F( s* q
- HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);
复制代码 打开定时器模式
! N- | x5 P9 d) L; k
# S) Y: c* g' q# ^8 X3 {- while (1)
' Z2 |3 [ `! I& S' d9 r" [; x - {. S2 q: X% i0 S/ }4 U
-
5 o9 A& A# n J$ c - GetData = __HAL_TIM_GET_COUNTER(&htim4);
- |' j% j. {7 x8 \) I" f- s$ c - Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim4); 2 \, m( j. `4 d
- sprintf((char *)data, "fre:%4d dir:%d",GetData*2,Direction);
+ T' k' Q& H; E K -
0 x9 p4 ]3 `% Y& ]* b- S - OLED_Clear();2 ^0 h* _+ ?4 \# x; o M, l
- OLED_ShowString(0,0,data);
8 O- }# A3 i( G, f2 L& t7 J - __HAL_TIM_SET_COUNTER(&htim4,0);
$ ~8 `9 n9 G# k# }! I; G- b - HAL_Delay(500);
/ t& S) B5 s7 K. m& e |5 m - /* USER CODE END WHILE */- H; J/ W+ j* O5 d
- 2 v, B' ?1 y5 t( D* @- H
- /* USER CODE BEGIN 3 */
% H: R B5 ? X4 O: v$ K( a0 e - }
复制代码 读取相关值并通过OLED显示出来- i" ?6 \% ?) z4 j1 y
1 k- j" @2 e* M, I
% j5 Y& C7 t' B0 w实际效果
4 {8 ^- v8 U$ t* P$ U$ J* v 用函数发生器模拟两列方波,配置编码器时我采用T1 and T2模式,分频系数为3,所以1s内采集到的脉冲个数为( 50 * 4 ) / ( 3 + 1 ) = 50,延时时间为500ms,所以OLED显示的值为 50 / 2 * 2 = 50 。
; X9 [0 i6 [9 @- ]. l3 x) F/ }( J9 |, J& M# T1 v6 a7 P w
: m! g+ a$ W/ e T& l
9 D; G& l4 l$ {$ Y$ v
. ?; J% g1 M- V6 v& w d: u8 A
实际效果% J2 y6 M; }- |# j3 l
# _$ z1 n: r9 X+ }
6 W- ?% e( t( N3 X0 N2 K, s+ W h% _% r
8 i0 s, \0 D+ y8 k
M( Q/ o/ n8 l. `9 {' _! f |