From c07279d0e75a3305d5b8eb0936f2546cb0f03b41 Mon Sep 17 00:00:00 2001 From: Namlak Date: Sun, 29 Jul 2018 20:40:24 -0700 Subject: [PATCH 01/41] Update getting-started.md Fixed link to Interfaces, added .md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index ee221541..b0bbb489 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -230,7 +230,7 @@ The ODrive also supports velocity control and current (torque) control. You can now: * See what other [commands and parameters](commands.md) are available, including setting tuning parameters for better performance. - * Control the ODrive from your own program or hook it up to an existing system through one of it's [interfaces](interfaces). + * Control the ODrive from your own program or hook it up to an existing system through one of it's [interfaces](interfaces.md). * See how you can improve the behavior during the startup procedure, like [bypassing encoder calibration](encoders.md#encoder-with-index-signal). If you have any issues or any questions please get in touch. The [ODrive Community](https://discourse.odriverobotics.com/) warmly welcomes you. From cd06834ade73fb552add8daeadb35a592f71337c Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Mon, 6 Aug 2018 17:55:10 -0700 Subject: [PATCH 02/41] Update odrivetool.md --- docs/odrivetool.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index d3c4b67a..375df993 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -77,7 +77,7 @@ To compile firmware from source, refer to the [developer guide](developer-guide) * If the DFU script can't find the device, try forcing it into DFU mode.
How to force DFU mode (ODrive v3.5)
- Flick the DIP switch that "DFU, RUN" to "DFU" and power cycle the board. If that alone doesn't work, also connect the After you're done, put the switch back into the "RUN" position and power cycle the board again. + Flick the DIP switch that "DFU, RUN" to "DFU" and power cycle the board. After you're done, put the switch back into the "RUN" position and power cycle the board again.
How to force DFU mode (ODrive v3.1, v3.2)
From 3089069f1def939d4c6b252124195641708852d6 Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Sat, 11 Aug 2018 14:50:10 +1000 Subject: [PATCH 03/41] Updated: how to increase amplifier gains --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 563b172f..ae2bef2b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -163,7 +163,7 @@ In the previous step we started `odrivetool`. In there, you can assign variables For instance, to set the current limit of M0 to 10A you would type: `odrv0.axis0.motor.config.current_lim = 10` Enter
-* The current limit: `odrv0.axis0.motor.config.current_lim` [A]. The default current limit, for safety reasons, is set to 10A. This is quite weak, and good for making sure the drive is stable. Once you have tuned the drive, you can increase this to 75A to get some performance. Note that above 75A, you must change the current amplifier gains. +* The current limit: `odrv0.axis0.motor.config.current_lim` [A]. The default current limit, for safety reasons, is set to 10A. This is quite weak, and good for making sure the drive is stable. Once you have tuned the drive, you can increase this to 75A to get some performance. Note that above 75A, you must change the current amplifier gains by requesting a different current range. i.e. for 90A on M0 'odrv0.axis0.motor.config.requested_current_range = 90' [A], then save the configeration and reboot as the gains are written out to the DRV (MOSFET driver) only during startup. * Note: The motor current and the current drawn from the power supply is not the same in general. You should not look at the power supply current to see what is going on with the motor current.
Ok so tell me how it actually works then...
The current in the motor is only connected to the current in the power supply _sometimes_ and other times it just cycles out of one phase and back in the other. This is what the modulation magnitude is (sometimes people call this duty cycle, but that's a bit confusing because we use SVM not straight PWM). When the modulation magnitude is 0, the average voltage seen across the motor phases is 0, and the motor current is never connected to the power supply. When the magnitude is 100%, it is always connected, and at 50% it's connected half the time, and cycled in just the motor half the time. From ada46be933f276e8ea6fe4a512cebb720512397b Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 10 Aug 2018 23:16:40 -0700 Subject: [PATCH 04/41] Update getting-started.md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index ae2bef2b..844e99e8 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -163,7 +163,7 @@ In the previous step we started `odrivetool`. In there, you can assign variables For instance, to set the current limit of M0 to 10A you would type: `odrv0.axis0.motor.config.current_lim = 10` Enter
-* The current limit: `odrv0.axis0.motor.config.current_lim` [A]. The default current limit, for safety reasons, is set to 10A. This is quite weak, and good for making sure the drive is stable. Once you have tuned the drive, you can increase this to 75A to get some performance. Note that above 75A, you must change the current amplifier gains by requesting a different current range. i.e. for 90A on M0 'odrv0.axis0.motor.config.requested_current_range = 90' [A], then save the configeration and reboot as the gains are written out to the DRV (MOSFET driver) only during startup. +* The current limit: `odrv0.axis0.motor.config.current_lim` [A]. The default current limit, for safety reasons, is set to 10A. This is quite weak, and good for making sure the drive is stable. Once you have tuned the drive, you can increase this to 75A to get some performance. Note that above 75A, you must change the current amplifier gains. You do this by requesting a different current range. i.e. for 90A on M0: 'odrv0.axis0.motor.config.requested_current_range = 90' [A], then save the configeration and reboot as the gains are written out to the DRV (MOSFET driver) only during startup. * Note: The motor current and the current drawn from the power supply is not the same in general. You should not look at the power supply current to see what is going on with the motor current.
Ok so tell me how it actually works then...
The current in the motor is only connected to the current in the power supply _sometimes_ and other times it just cycles out of one phase and back in the other. This is what the modulation magnitude is (sometimes people call this duty cycle, but that's a bit confusing because we use SVM not straight PWM). When the modulation magnitude is 0, the average voltage seen across the motor phases is 0, and the motor current is never connected to the power supply. When the magnitude is 100%, it is always connected, and at 50% it's connected half the time, and cycled in just the motor half the time. From b9424461dd234b569a1fa18109aeb3246a3e451b Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Tue, 14 Aug 2018 19:45:53 +1000 Subject: [PATCH 05/41] Added position, velocity and current definitions --- docs/commands.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/commands.md b/docs/commands.md index 0ec87a36..177e57cf 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -71,6 +71,18 @@ An upcoming feature will enable automatic tuning. Until then, here is a rough tu * Back down `pos_gain` until you do not have overshoot anymore. * The integrator is not easily tuned, nor is it strictly required. Tune at your own discretion. +## System monitoring commands + +### Encoder position and velocity +* View encoder position with `.encoder.pos_estimate` [counts] +* View rotational velocity with `.encoder.ppl_vel` [counts/s] + +### Motor current and torque estimation +* View the commanded motor current with `.motor.current_control.Iq_setpoint` [A] +* View the measured motor current with `.motor.current_control.Iq_measured` [A]. If you find that this returns noisy data then use the command motor current instead. The two values should be close so long as you are not approching the maximim achieveable rotational velocity of your motor for a given supply votlage, in which case the commanded current may become larger than the measured current. + +Using the motor current and the known KV of your motor you can estimate the motors torque using the following relationship: Torque [N.m] = 8.27 * Current [A] / KV. + ## General system commands ### Saving the configuration From 81691eecbd7e8003de6ada652427a6367445cc6a Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Tue, 14 Aug 2018 16:44:37 -0700 Subject: [PATCH 06/41] Update commands.md --- docs/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index 177e57cf..16710ab0 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -75,7 +75,7 @@ An upcoming feature will enable automatic tuning. Until then, here is a rough tu ### Encoder position and velocity * View encoder position with `.encoder.pos_estimate` [counts] -* View rotational velocity with `.encoder.ppl_vel` [counts/s] +* View rotational velocity with `.encoder.pll_vel` [counts/s] ### Motor current and torque estimation * View the commanded motor current with `.motor.current_control.Iq_setpoint` [A] From bdc175317e8482a04c2fc2bcde47f4c068aea989 Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Wed, 15 Aug 2018 21:30:13 +1000 Subject: [PATCH 07/41] Added a brief overview of liveplotter --- docs/odrivetool.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index 375df993..f1737d72 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -131,3 +131,39 @@ adapter speed: 2000 kHz ``` If something doesn't work, make sure `openocd` is in your `PATH` variable, check that the wires are connected properly and try with elevated privileges. + +## Liveplotter + +Liveplotter is used for the graphical plotting of odrive parameters (i.e. position) in real time. To start liveplotter, close any other instances of liveplotter and run `odrivetool liveplotter` from a new anaconda prompt window. By defult two parameters are plotted on startup; the encoder positon of axis 1 and axis 2. In the below example the motors are running in open `closed_loop_control` while they are being forced off position by hand. + +Image here + +To change what parameters are plotted open odrivetool (located in Anaconda3\Scripts or ODrive-master\tools) with a text editor and modify the liveplotter function: +``` + # If you want to plot different values, change them here. + # You can plot any number of values concurrently. + cancellation_token = start_liveplotter(lambda: [ + my_odrive.axis0.encoder.pos_estimate, + my_odrive.axis1.encoder.pos_estimate, + ]) +``` +For example, to plot the approximate motor torque [N.cm] and the velocity [RPM] of axis1 with a 150KV motor and an 8192 count per rotation econder you would modify the function to read: +``` + # If you want to plot different values, change them here. + # You can plot any number of values concurrently. + cancellation_token = start_liveplotter(lambda: [ + (((my_odrive.axis0.encoder.pll_vel)/8192)*60), # 8192 CPR encoder + ((8.27*my_odrive.axis0.motor.current_control.Iq_setpoint/150) * 100), # Torque [N.cm] = (8.27 * Current [A] / KV) * 100 + ]) +``` +In the example below the motor is forced off axis by hand and held there. In response the motor controller increases the torque (orange line) to counteract this disturbance up to a peak of 500 N.cm at which point the motor current limit is reached. When the motor is released it returns back to its commanded position very quickly as can be seen by the spike in the motor velocity (blue line). + +Image here + +To change the scale and sample rate of the plot modify the following parameters located at the beginning of utils.py (located in Anaconda3\Lib\site-packages\odrive): + +`data_rate = 100 +plot_rate = 10 +num_samples = 1000` + +For more examples on how to interact with the plotting functinality refer to the [Matplotlib examples.](https://matplotlib.org/examples) From 8ffc4003540ea5ce1112a68af81d2dee332abdac Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Wed, 15 Aug 2018 21:31:48 +1000 Subject: [PATCH 08/41] images for liveplotter overview --- docs/figure_1-1.png | Bin 0 -> 21221 bytes docs/figure_1.png | Bin 0 -> 28413 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/figure_1-1.png create mode 100644 docs/figure_1.png diff --git a/docs/figure_1-1.png b/docs/figure_1-1.png new file mode 100644 index 0000000000000000000000000000000000000000..036ca2625a1c0eaa67659c4eb98116eb429470c9 GIT binary patch literal 21221 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∾Q^D2gSX35d=oJ-$tf~D~k z?GYM85r@Z^3G*AD$K~Bm+-{LSkBOHLeA>4E-;mu!%;sjg%`Q}<>6=|k zqjfT-w_l3E@>sTx)d$+Z(ZQH(3~Hd(ISW!FB5K^f{wizHUC!Y44PDEnYiXyS*Q7?k z%p_RbP>8VI*>mm^UlN*~N>`fsErF?cKciEE%b13lU*7}@fG4gg#2l7N6q{B(>*!D) zDK`0xJDSG*L;bwOY@#_#F~!1<)zHweva)i0K_@I-^(V2$f2~D#c$e62h5F|1{!?9; zOAV;}({kS6T!~ThCfSw21F5%flzq!g4-1Ajj9xydJbjzpVdz1nUt6bvX-RX=L~{Q> z)FX>JJ&@y<99~~Y`p|s1@B*vA>GZG2GUAEfg2Ty}{RL6{Ga?E~Q zHHPIyX~GTb%61M8MTLdxadMlgGFH^uFBDs;XR~i~KV#)rWHYe4P;@@gO90*2g@{op zQQ=B3Fc3HJFvMQ;ohxaGzyyd-5o z^)4g0__2f#L3Y9E%loaZ&gf-oYYoMDn9OAT5qVSyN@pf6tI4E%x5Db1GXsa-?2_>c z2U)8)PDM+IWEO-^bI1g>_gksm#>-X04kmF1y1K6`dKJv*<+om}HXXRjuVcxmA1j+y zyQZ0%nQb2udIe8YFF-$BAFQj|f;cWrz~Yl7dScY?>~pOVqjPrT*qL{Fh{~w2=t8Dl zx-VF-GvQk`NXKqmjKGUc0c)F>k+xoNY?UZ*use_N6gQ?t?EdU~eM5kMLF%Jdce;4` zHM?g50s@)s_TB04AWgb6BxT)%>wj}UR(}0R0?t{+b5?0zCXVUF1RFL)YlE6AV{%70 zNg7SrR9>obibyArj~v4RM=fuswRQzGZTuOP*eb!<-Gw3bNHjv^fiIY93i+5Fw8(!1 zlb8HCiZB=*s<4Di%+k`5zWZQea`G7!?)XRawvjlPx4!F#qBmj2Qp2;LVaHr%fw;mU zJJz@%;lPQVqeX|moCgyGVp}p^fGxL>Cg9r(Dm1LniVIe-!f=|^xNH5%h9E*NJImws zhnt;M)-4g0Tiy!;?*y(~@lhx&EF9RJfAcXmQ7I@a|6o^PNy|r$X$qCnp@1gGZk0u$ z2Sq9S=+OBOlwrNmjr(-7?EX`)_XzX56ZVIHV%_*)w{Ia%(argsu%>L(YTYA0c$iFU z+$}!Y(9+WOc<&5{UNOnYo`u7q`oya}!d3c_kB6LmNLwV4vqkox)B!!#ld zTwy75@10#-s%*b8ic3foHZ+(gRC@hVXFh$}aUfSsJ3}SXgyn0UvC;GzmvMNGMNwS)E)(*#2k8ta@7)jy2VEw=h~hJ z&1d=h;6iqtzJGeG9q@6nu;?`PV;@+u=K^aUgSLl_x4$sy@EGihR*v(9v+#z!J-F7? z;-+S)${YAPcjZ-+F`|YoId-{1fe+A?GFg%{LPx?IeL`x#p958@)eC@OWn{)0l$h7) zP1bmf!fA-xccng>Uw(~M;(GO~Gi50+!(GqKtz>g^^SZ~3oIG>@Yfpaqq+D|MgEGxo z;h~`deQ~d!szR0xsY2G~UviWwJk8#2<8N(?P=u_I1$gHBb_*wz(+m#sVW`<}Q;42P z`tdijZH|J@$P#kP>-sr=~ORwyWLNr8uo|kMY$J&A>Oiy$%8cO;@A6^7I1sFH$yNFDM*K*_Mv-0KWMK6F8`ljUN-Py$UUdmZDa-~{5 zD{nwAULzf?q|IsgqSU=ErDD%Bq2%^^kQdiGN(@Bzf1$P(h(<3h3t7Jh$iG8ui&J{e zl|D?!T}&{4-sP~AtuRjLNpMrp1e%!R>=TR_(DpMizIk+bBjNOOR)=TgFynvb@7#tC z*s%3Mf?qWio-SIcD{nCL>CcKrdZP!0rg}@Pm%;CfSnnkly4jt!&(2P9{IZ>x#e}K3 z6<%=mBKVZFFLN^2hC+1s`OB>{Qgo8)E8PjbBFtW!TBck6R8sa?NXBT7<-Soi>~4(- zN+w&f)Bq7iT@-^jCHr@weBone=9<4Y@a}u=Z&eBD80*5Cj-kd?UIybHUG?7P!=y*{p)wf^oFf2RxLeYbpB~M^EWThdwyAvxs(jD4Srw*` zN~8=pk6C_IZ{JvC~;9g1YR^or4Mt6QBxciUjEv1b9AaGe{QyE#@^z#Hmulf=sJlm zFamSYN{2=iWi`_19c~6B*Q^_9TP|B44ZA(RbOLK}P_l?qYn5huFl?nwQ8w5hhC+P( zsy~S16oGYLd3O;p)#{6P$rD`ZUR`b5V=WNqk^Z27^b|*OnSUz<)z$vAXd_>nQy%U| zql1=cKX64>=;&yp{MZOgmV_Kl9NkmUG>#}+d0+9(8#7T{Xn7kfOCu&|lmuwXnd!>aWM=LDo^56&BfuGz zd4I{-<6sjPSK?xs_R=gNO(D>*cJLCuz?EV!R1;oCO02Cu@c)2hQDTDxdwCsjl+8l@UX`vFHB+g39kbOr{H#sgBv7wXC1~xRr4NaDYI}C9 zE6EPqe1NnM@_blgunl{nRP{@J3xFZ<5oLJ;Ah{|7m9nD3%NIku2B4C`{F4W6N@f2U93ITv;k&9^+1#-0;J z^G+-@p{^+)OCO;bLT7y?wE^ka8z=OYPVWMx5_lvleFJ-vS;>X!Pw`c9?3~|fa6v$( zFJtR4@pgw`w@+L{ks_imo9y=$>mq#6*@* z+_5+tUa4Z!!)79Y20W0&9rFZ!S(X<(Rh7O5;I=;Ei)Vk%+u7LFlp%iLR{&O}$BG@h zE<@?C`OkO6$|c|rXI@oaQQX`F@6*aVrd>@nvDtS@Giu-mJmw{ya)3i>qLA$N zB#+u#>i6J&s;V@+YNgV=)NN7zAB{l?_{xz6T;bI<(hjDZHQu;b>U9Ib{L{0RHu9-~ z4pMre0O?4nH0TX-kyKd7i(STVU}c-Ck=DE!N;Xnz+dK>?DybT83egYWMx#ok(d0@% z0urv14g$hfAZOnzycd*8qz@O? z*sxQK6baB>H9?17Mo@9Z0;*B!cBopTGIbGO$`Te8&{;+ZJ{t6MIFbd49y<(WB?;ik z_gsPk+Tx5ak5p}EeeC;XDsd8_hdh{QwXx(kkT%{d`_Ybz}4chBD9vKvtn|C z#awf-%iaPzqHORU_W1^Np!?-lhoVJ45qC+{i&!pxgh?ZNa<8=%hI5UPoYNxgPL&Fr zf$4`$tISG~VTi3qaC!nAE9XD~xX-ddnX4pM+1k6mg6|81oUiV5kFjVUyoWZ4hVoY~ z0_1Pf=ZkEhqT*1X>)YZx@mb~9e?&dYsjc-Jec5&S)v~I?7Zk|N6(XQc>B9xjk_=^2 zdV-upc-#8iG6iIRW0j-zu`Il5@}(<`8hIk`_HlnOVK5xWPGr^E``H&5)iy*-%!$u*b$%n7?*~eu3a>@zJTg$q$)sC9o zCAw;Qbi<0kqE-S_S62RpsFRQl4MJO&F?+JkJ0IVb_I7H@JSin*sYiCr0EI#+XY?u@ zXr8qUcwJsm0i7K0ZgT|kWKC0|o3^WIyGr9qmi$B0{s?WTTe)1%vsp0$T|Hhaire7C zQx~Ek3HjuDkk4*+*xUa1q`pT+j6(i=kAhGwDO*3HMVuiLg?7e&JOeNT?$**MW)vc4 z6jE{oDD}$kn#))r!Itp&tvKvjaWOqv6DtMgd>xb?e-!4sv~KEZ-K(I;knRAe2~f4E2RoqI0nJW=T+ zUtRFO81I>vb-4kcLy_JH_-}?hV7dOy2*G*OR*z5iBPPFMrYfQ4idF(!uBzl%Xg1$g z$ia-pm6+8)keF;eef`AM#@(5i|H)3jHb4(J;){`EwZpIzpM&ILZEqlZCY%A1_#EQ_ zn1Q8G>55^Y7`L`KPRwwTu?uu$P)B_}*$eS`w7lnghBZVQbD?Jpjw1YhFEa9knx38A zJMSt^jgdS)i1v$dZ$9#yUsB~!rXm*&C{MV8+&k16@m+pA-Sp1pA60lRN7$6kgoEGC zaL(oB*Px-lJ-+&lFJ}l7cH}a@B#+v0gkCq2RVyB{@1sVtJCm=NylHE@1&^vmf>Eh? zK|};nEB*R6rwYe@?y9OPenG)bC+`WT{DRtAsf8R)x~Pwjbs(*JbUknL*vQ}y*k>Fp zbkh1)%owY~@6mCgdCa#l%D%o;mvpk1gGnh(3Au?n3MMvfG4VXznbP`+m$U}NN6p_p zKQFnM7&>sK6eb(#ZHP|$DY(Kx)cn;nQx3nCuvQWm)Y z&mTw=JuV&iSu=A z{y^nQw8j-o9X<7xcXE#FUZI{H)e?HvR@}Ya6Y8ecbe$2ph)c9ZwS^Kz}H=H2_wb| zS;MyVP&Q)kB%5(F$5klcHXW8^Hiv$8-*`$mf-C-gl0*g>*>(0<3^~VD0jI$!&mSjq z;3DNOkeV3>dHopOvs25!>mtZsAdy_)l&=MuwPkOHk_J1JZm|C#NN)7&(yf<$bSt-f z!7Y6(r9J_k!o6FaCHQ92Okq3-w8{>kmi-160Kjnrc6jd+8|R7a2SJ-hNJPMS_;=B+ z!+R|?+hN2#t7-4?FPi=oSOyi+hxX1|AM!>{JJg>Ck)m)=0OTTJ*I@Q4qzV9WkS+iO zoPu-%SR1(h{|o@YIt!!@K)1sU`9C&^NLxi`u-fpb#jEmsmWSUKl;dhvb{~jvgKTtO z-X%3XDBL1ZaFG3?f&#V2j~}P4-n#WCCS`8zLK|w*bkGn;=>K){nGx+czlVfUri>2$pHAWY*+FNlPLe030itq11HfY9SuG+Mn zTFlYCa|f#^>Vyr^rvD$!9{T1qn(kEuitUir$?~6dB1viMWNEA9L1(DQeV|uRki|AP zRP@1rA8A%0pK>>Fy^M;SL>4GU{&xhyUiiI`pEEM{!6qpyt7Ks?-vxqJr>!QqT(PJ( zAfc&WzyF1bB=&DB?#C}KE;3HA*i^!&L!O@#SC^KSPQPjs2_eXn`|z*_(^}8h>KYnJ zIXRe|g8=gSBQW=*HC7){fOy*s~=`>5~qvUO5MAoWws-O~j)$m#yFZn|r}QglW>cb2R%q&f7U|K+!zqv+}Pm%POvUVsR1mRDcj`6@>|B*5%|ZhI)B^WMMz zdj0x!+j^H$QJ0Y`gikagsZe62(Vmw`T?<)5<<-|uK!EZwC&Kz?`V8p1B;YeV(jkLS z=RBOHw4bVdkXKhX?&J;SaXQ@Z3g=+~0u)NA9x#%ChoOSrFB<1IA)c2*TP(Xvj~qeM z8zSG!#ChjXwn0EIEiJ9*X^LS@CLjGu_BK03x@FfIY^ptQtCim&1$Z@*QwIk0DuVaC zDs0;62oo-tw8O(-0(9C+xz+X_?LwUNH0WPWgK*;0s@FUkHg0_)kooGs5Qkfj%+eJ? z|835+U0fJ0)2o9Y-yw+6>~!%FM9wmJL-6l3l3=pB{Z>~jXv?v}XJO#fPDkUp^XH%Q z5FjWXt4++)*86$oUJVP(c7td&1Ixm zZHW>lq|%&A1JArDTKHI0T(JY}n5 zk-U-Us2A}UZwoL``It=)_4U2#=;%1L-I3+qv}Z45)nvZbbWjTScTUb7*FLlQ0IG;E z=H|OT+mh?n;C{5f?d!}W%&Q6N02PCiynm+>t$Dlz>f2j5Ssgx7m`WW>JAVf9{2KP=ZPmYfN=QDlr6 zseLfVn9V8ao(Y4SOsfxg82jN7$U@0Mb<7UpURD+q6%E4a>=7CKp<%N~=v5qr7FA}P z(_o&{>QvoC*}aEQaqbP_;5us#m7`3 zhkMCU!~VXsR06J4A}4tBDU4`|Lj^i5K&`Ytkh$#Y)O&^bU`uDRc_a* z>F!<{2(MI@#MBa#Bn$v~52*1;PhuO6_m}K{evQXNM&LE!nR5UY&Tqlc{KCWM7Q_>} z=He9H6^^#_2~f#pwK=hx-IFapnF&vuJrt*ShzTglbV<|te5wKihcwA;gwv{R_~>VWuP)@4Q9Hx`3;6TaBd{pBfBw|B36%DUX{{ug++NPIkf zanU(3D~n|EctC(tTL^rbDKRyFjM|;{M;%HA&NKwJrG|gZ#|0Cf*TLP;3Jq=IzkFF# z@nF1H6F#O=ZRsK5MQs6784nh-g?CTK)@V(^S`Q9w;a`(~-K Hb;N%GUqUJF literal 0 HcmV?d00001 From 707a135bbfb84075405dfaf3a41e46ec7c1ceb07 Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Wed, 15 Aug 2018 21:37:14 +1000 Subject: [PATCH 09/41] added figures --- docs/odrivetool.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index f1737d72..fd2b0878 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -136,7 +136,7 @@ If something doesn't work, make sure `openocd` is in your `PATH` variable, check Liveplotter is used for the graphical plotting of odrive parameters (i.e. position) in real time. To start liveplotter, close any other instances of liveplotter and run `odrivetool liveplotter` from a new anaconda prompt window. By defult two parameters are plotted on startup; the encoder positon of axis 1 and axis 2. In the below example the motors are running in open `closed_loop_control` while they are being forced off position by hand. -Image here +![Liveplotter position plot](figure_1.png) To change what parameters are plotted open odrivetool (located in Anaconda3\Scripts or ODrive-master\tools) with a text editor and modify the liveplotter function: ``` @@ -158,12 +158,14 @@ For example, to plot the approximate motor torque [N.cm] and the velocity [RPM] ``` In the example below the motor is forced off axis by hand and held there. In response the motor controller increases the torque (orange line) to counteract this disturbance up to a peak of 500 N.cm at which point the motor current limit is reached. When the motor is released it returns back to its commanded position very quickly as can be seen by the spike in the motor velocity (blue line). -Image here +![Liveplotter torque vel plot](figure_1-1.png) To change the scale and sample rate of the plot modify the following parameters located at the beginning of utils.py (located in Anaconda3\Lib\site-packages\odrive): -`data_rate = 100 +``` +data_rate = 100 plot_rate = 10 -num_samples = 1000` +num_samples = 1000 +``` For more examples on how to interact with the plotting functinality refer to the [Matplotlib examples.](https://matplotlib.org/examples) From 23ab2af36f9af30bf48cad64c60787821b73be68 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Thu, 16 Aug 2018 15:16:39 -0700 Subject: [PATCH 10/41] Update odrivetool.md --- docs/odrivetool.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index fd2b0878..90e71ee3 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -134,7 +134,7 @@ If something doesn't work, make sure `openocd` is in your `PATH` variable, check ## Liveplotter -Liveplotter is used for the graphical plotting of odrive parameters (i.e. position) in real time. To start liveplotter, close any other instances of liveplotter and run `odrivetool liveplotter` from a new anaconda prompt window. By defult two parameters are plotted on startup; the encoder positon of axis 1 and axis 2. In the below example the motors are running in open `closed_loop_control` while they are being forced off position by hand. +Liveplotter is used for the graphical plotting of odrive parameters (i.e. position) in real time. To start liveplotter, close any other instances of liveplotter and run `odrivetool liveplotter` from a new anaconda prompt window. By defult two parameters are plotted on startup; the encoder positon of axis 1 and axis 2. In the below example the motors are running in `closed_loop_control` while they are being forced off position by hand. ![Liveplotter position plot](figure_1.png) @@ -169,3 +169,10 @@ num_samples = 1000 ``` For more examples on how to interact with the plotting functinality refer to the [Matplotlib examples.](https://matplotlib.org/examples) + +### Liveplotter from interactive odrivetool instance +You can also run `start_liveplotter(...)` directly from the interactive odrivetool prompt. This is useful if you want to issue commands or otherwise keep interacting with the odrive while plotting. + +For example you can type the following directly into the interactive prompt: `start_liveplotter(lambda: [odrv0.axis0.encoder.pos_estimate])`. Just like the examples above, you can list several parameters to plot separated by comma in the square brackets. +In general, you can plot any variable that you are able to read like normal in odrivetool. + From 90801c887f3b90e3efaa483e60882523c7a33a2b Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Mon, 20 Aug 2018 22:00:20 +1000 Subject: [PATCH 11/41] Mention that reboot should be called after saving a config https://github.com/madcowswe/ODrive/issues/183 --- docs/getting-started.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 844e99e8..4bcdb482 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -197,8 +197,11 @@ For instance, to set the current limit of M0 to 10A you would type: `odrv0.axis0 ### 3. Save configuration. -You can save all `.config` parameters to persistent memory such that the ODrive remembers them between power cycles. -* `odrv0.save_configuration()` Enter +You can save all `.config` parameters to persistent memory such that the ODrive remembers them between power cycles. +* `odrv0.save_configuration()` Enter. + +Due to a [known issue](https://github.com/madcowswe/ODrive/issues/183) it is strongly reccomneded that you reboot following every save of your configerations using `odrv0.reboot()`. + ## Position control of M0 From 0f743939ecd243657ddc008c93bce5bb5249e0c5 Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Thu, 23 Aug 2018 22:14:23 +1000 Subject: [PATCH 12/41] added calibration_current for small motors reference Akex_ was unable to calibrate a small 15A peak current motor. Reducing `calibration_current` from 10A to 5A resolved the issue. I have included a reference to the use of small motors and the calibration current. --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 844e99e8..3e0ca944 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -171,7 +171,7 @@ For instance, to set the current limit of M0 to 10A you would type: `odrv0.axis0 The largest effect on modulation magnitude is speed. There are other smaller factors, but in general: if the motor is still it's not unreasonable to have 50A in the motor from 5A on the power supply. When the motor is spinning close to top speed, the power supply current and the motor current will be somewhat close to each other.
* The velocity limit: `odrv0.axis0.controller.config.vel_limit` [counts/s]. The motor will be limited to this speed; again the default value is quite slow. -* You can change `odrv0.axis0.motor.config.calibration_current` [A] to the largest value you feel comfortable leaving running through the motor continously when the motor is stationary. +* You can change `odrv0.axis0.motor.config.calibration_current` [A] to the largest value you feel comfortable leaving running through the motor continously when the motor is stationary. If you are using a small motor (i.e. 15A current rated) you may need to reduce `calibration_current` to a value smaller than the defualt 10A. ### 2. Set other hardware parameters: From d37cc446c851d406048e2877ea69ba80500ef243 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Thu, 23 Aug 2018 14:49:35 -0700 Subject: [PATCH 13/41] Update getting-started.md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 4bcdb482..bf6f1f60 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -200,7 +200,7 @@ For instance, to set the current limit of M0 to 10A you would type: `odrv0.axis0 You can save all `.config` parameters to persistent memory such that the ODrive remembers them between power cycles. * `odrv0.save_configuration()` Enter. -Due to a [known issue](https://github.com/madcowswe/ODrive/issues/183) it is strongly reccomneded that you reboot following every save of your configerations using `odrv0.reboot()`. +Due to a [known issue](https://github.com/madcowswe/ODrive/issues/183) it is strongly recommended that you reboot following every save of your configuration using `odrv0.reboot()`. ## Position control of M0 From bd89f90615ace6deb5e9149c26f87aea2218d80f Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Thu, 23 Aug 2018 14:51:04 -0700 Subject: [PATCH 14/41] Update getting-started.md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 3e0ca944..8bc42c33 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -171,7 +171,7 @@ For instance, to set the current limit of M0 to 10A you would type: `odrv0.axis0 The largest effect on modulation magnitude is speed. There are other smaller factors, but in general: if the motor is still it's not unreasonable to have 50A in the motor from 5A on the power supply. When the motor is spinning close to top speed, the power supply current and the motor current will be somewhat close to each other. * The velocity limit: `odrv0.axis0.controller.config.vel_limit` [counts/s]. The motor will be limited to this speed; again the default value is quite slow. -* You can change `odrv0.axis0.motor.config.calibration_current` [A] to the largest value you feel comfortable leaving running through the motor continously when the motor is stationary. If you are using a small motor (i.e. 15A current rated) you may need to reduce `calibration_current` to a value smaller than the defualt 10A. +* You can change `odrv0.axis0.motor.config.calibration_current` [A] to the largest value you feel comfortable leaving running through the motor continously when the motor is stationary. If you are using a small motor (i.e. 15A current rated) you may need to reduce `calibration_current` to a value smaller than the default. ### 2. Set other hardware parameters: From ed2c72d8396fbc72acb0353858a5aab31575145a Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Sat, 18 Aug 2018 21:14:21 -0700 Subject: [PATCH 15/41] fix char to int conversion --- .../examples/ODriveArduinoTest/ODriveArduinoTest.ino | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino b/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino index a06c9818..f948f7f6 100644 --- a/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino +++ b/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino @@ -46,19 +46,20 @@ void loop() { // Run calibration sequence if (c == '0' || c == '1') { + int motornum = c-'0'; int requested_state; requested_state = ODriveArduino::AXIS_STATE_MOTOR_CALIBRATION; Serial << "Axis" << c << ": Requesting state " << requested_state << '\n'; - odrive.run_state(atoi(c), requested_state, true); + odrive.run_state(motornum, requested_state, true); requested_state = ODriveArduino::AXIS_STATE_ENCODER_OFFSET_CALIBRATION; Serial << "Axis" << c << ": Requesting state " << requested_state << '\n'; - odrive.run_state(atoi(c), requested_state, true); + odrive.run_state(motornum, requested_state, true); requested_state = ODriveArduino::AXIS_STATE_CLOSED_LOOP_CONTROL; Serial << "Axis" << c << ": Requesting state " << requested_state << '\n'; - odrive.run_state(atoi(c), requested_state, false); // don't wait + odrive.run_state(motornum, requested_state, false); // don't wait } // Sinusoidal test move From 1d31d23d0e5297c0207b3e217d1300f4d96a0bca Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Sun, 26 Aug 2018 13:42:32 -0700 Subject: [PATCH 16/41] Update hoverboard.md --- docs/hoverboard.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/hoverboard.md b/docs/hoverboard.md index 54cd70f5..5cf64332 100644 --- a/docs/hoverboard.md +++ b/docs/hoverboard.md @@ -33,6 +33,7 @@ Lets also start in velocity control mode since that is probably what you want fo odrv0.axis0.encoder.config.bandwidth = 100 odrv0.axis0.controller.config.pos_gain = 1 odrv0.axis0.controller.config.vel_gain = 0.02 +odrv0.axis0.controller.config.vel_integrator_gain = 0.1 odrv0.axis0.controller.config.vel_limit = 1000 odrv0.axis0.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL ``` From a2e0ea6374c4ad13637245f600df39de80b93123 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Mon, 27 Aug 2018 22:49:44 -0700 Subject: [PATCH 17/41] add DfuSeDemo tool instructions --- docs/odrivetool.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index 90e71ee3..b2f8cf9e 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -84,6 +84,37 @@ To compile firmware from source, refer to the [developer guide](developer-guide) Connect the pin "BOOT0" to "3.3V" and power cycle the board. If that alone doesn't work, also connect the pin "GPIO1" to "GND". After you're done, remove the wires and power cycle the board again. +### Upgrading firmware with a different DFU tool +Some people have had issues using the python dfu tool, so below is a guide on how to manually use a different tool. + +Before starting the below steps, you need to get firmware binary. You can download one of the officially released firmware files from [here](https://github.com/madcowswe/ODrive/releases). Make sure you select the file that matches your board version, and that you get the __.hex__ file (not the __.elf__ file). + +To compile firmware from source, refer to the [developer guide](developer-guide). + +#### Windows +You can use the DfuSe app from ST. + +1. Download the tool [here](https://www.st.com/en/development-tools/stsw-stm32080.html). Unfortunately they make you create a login to download. Sorry about that. +1. After installing the tool, launch `DfuFileMgr.exe` which probably got added to the start menu as "Dfu file manager". +1. Select "I want to GENERATE a DFU file from S19, HEX or BIN files", press OK. +1. Click the button that says "S19 or Hex...", find the `ODriveFirmware.hex` file you built or downloaded. +1. Leave all the other settings as default and click the "Generate..." button. +1. Save the output file as `ODriveFirmware.dfu`. Note that the success message has a warning sign for some reason... +1. Launch `DfuSeDemo.exe` which probably got added to the start menu as "DfuSeDemo". +1. Force the ODrive into DFU mode, as per the instructions above "How to force DFU mode". +1. In the top left it should now be connected to "STM Device in DFU Mode". + 1. If it doesn't appear, it may be because the driver is set to libusb by Zadig. We need to set it back to the original driver. Follow [these instructions](https://github.com/pbatard/libwdi/wiki/FAQ#Help_Zadig_replaced_the_driver_for_the_wrong_device_How_do_I_restore_it). +1. In the bottom right section called "Upgrade or Verify Action" click the button "Choose...". +1. Locate the `ODriveFirmware.dfu` we made before. +1. Click button "Upgrade". +1. If you get a warning that it's not possible to check that it's the correct device type: click yes to continue. +1. Congratulations your ODrive should now be flashed; you can now quit DfuSeDemo. + +#### MacOS or Linux +**This section needs more detail. Please consider adding detail if you got it to work.** +You may be able to use [dfu-util](http://dfu-util.sourceforge.net/) to upgrade the firmware. You will need to convert the .hex file to a .dfu file. You may be able to do it with the python script [dfu-convert](https://github.com/plietar/dfuse-tool/blob/master/dfu-convert) or the c program [hex2dfu](https://github.com/encedo/hex2dfu). + +You probably need to force DFU mode, as per the instructions above. ## Flashing with an STLink From fb16534067f11ca7d5ab7b67af151c346fc49a96 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Mon, 27 Aug 2018 23:07:19 -0700 Subject: [PATCH 18/41] power cycle after dfu docs --- docs/odrivetool.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index b2f8cf9e..80d926b7 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -109,6 +109,7 @@ You can use the DfuSe app from ST. 1. Click button "Upgrade". 1. If you get a warning that it's not possible to check that it's the correct device type: click yes to continue. 1. Congratulations your ODrive should now be flashed; you can now quit DfuSeDemo. +1. Turn off the power to the ODrive and set the DIP switch back to RUN mode. #### MacOS or Linux **This section needs more detail. Please consider adding detail if you got it to work.** From b3bcdd5d5ea5fd9eb39d40885d5244812792136f Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Wed, 29 Aug 2018 23:56:43 -0700 Subject: [PATCH 19/41] fill out sensorless guide a bit more --- docs/commands.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 16710ab0..212a4899 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -100,10 +100,15 @@ All variables that are part of a `[...].config` object can be saved to non-volat ## Setting up sensorless The ODrive can run without encoder/hall feedback, but there is a minimum speed, usually around a few hunderd RPM. -However the +However the units of this mode is different from when using an encoder. Velocities are not measured in counts/s, instead it is electrical rad/s. This also applies to the gains. For example, `vel_gain` is in units of `A / (rad/s)` instead of `A / (count/s)`. + +To give an example, suppose you have a motor with 7 pole pairs, and you want to spin it at 3000 RPM. Then you would set the `vel_setpoint` to `3000 * 2*pi/60 * 7 = 2199 rad/s electrical`. + +Below are some suggested starting parameters that you can use. Note that you _must_ set the `pm_flux_linkage` correctly for sensorless mode to work. + ``` -odrv0.axis0.controller.config.vel_gain = 0.1 -odrv0.axis0.controller.config.vel_integrator_gain = 0 +odrv0.axis0.controller.config.vel_gain = 0.01 +odrv0.axis0.controller.config.vel_integrator_gain = 0.05 odrv0.axis0.controller.config.control_mode = 2 odrv0.axis0.controller.vel_setpoint = 400 odrv0.axis0.sensorless_estimator.config.pm_flux_linkage = 5.51328895422 / ( * ) From 71142a7c85633a6cf30e3c3e02de1530fd0ad2ab Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Thu, 30 Aug 2018 21:49:20 -0700 Subject: [PATCH 20/41] release fw-v0.4.3 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b7fc42..f024cb65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Unreleased Features Please add a note of your changes below this heading if you make a Pull Request. -## Unreleased +# Releases +## [0.4.3] - 2018-08-30 ### Added * Encoder position count "homed" to zero when index is found. @@ -14,7 +15,6 @@ Please add a note of your changes below this heading if you make a Pull Request. * Once you got an axis error `ERROR_INVALID_STATE` you could never clear it * Char to int conversion to read motornum on arduino example -# Releases ## [0.4.2] - 2018-08-04 ### Added * Hall sensor feedback From 0ded9368457d366434b1bdc755f90213381b1228 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Tue, 21 Aug 2018 14:24:49 -0700 Subject: [PATCH 21/41] usb split buffers WIP --- CHANGELOG.md | 1 + .../Class/CDC/Inc/usbd_cdc.h | 28 ++++--- .../Class/CDC/Src/usbd_cdc.c | 76 +++++++++++-------- Firmware/Board/v3/Src/usbd_cdc_if.c | 31 ++++++-- 4 files changed, 88 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f024cb65..96e8644d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Please add a note of your changes below this heading if you make a Pull Request. * `motor.config.requested_current_range` * `motor.config.current_control_bandwidth` and `motor.set_current_control_bandwidth`. Latter required to invoke gain recalculation. * `encoder.config.bandwidth` + * `sensorless_estimator.config.pm_flux_linkage` ## [0.4.1] - 2018-07-01 ### Fixed diff --git a/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h b/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h index 3bb73c6e..3f250554 100644 --- a/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h +++ b/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h @@ -109,19 +109,25 @@ typedef struct _USBD_CDC_Itf }USBD_CDC_ItfTypeDef; +typedef struct +{ + uint8_t* Buffer; + uint32_t Length; + volatile uint8_t State; +} +USBD_CDC_EP_HandleTypeDef; typedef struct { uint32_t data[CDC_DATA_HS_MAX_PACKET_SIZE/4]; /* Force 32bits alignment */ uint8_t CmdOpCode; - uint8_t CmdLength; - uint8_t *RxBuffer; - uint8_t *TxBuffer; - uint32_t RxLength; - uint32_t TxLength; - - __IO uint32_t TxState; - __IO uint32_t RxState; + uint8_t CmdLength; + + USBD_CDC_EP_HandleTypeDef CDC_Tx; + USBD_CDC_EP_HandleTypeDef CDC_Rx; + + USBD_CDC_EP_HandleTypeDef ODRIVE_Tx; + USBD_CDC_EP_HandleTypeDef ODRIVE_Rx; } USBD_CDC_HandleTypeDef; @@ -153,10 +159,12 @@ uint8_t USBD_CDC_RegisterInterface (USBD_HandleTypeDef *pdev, uint8_t USBD_CDC_SetTxBuffer (USBD_HandleTypeDef *pdev, uint8_t *pbuff, - uint16_t length); + uint16_t length, + uint8_t* odrivebuff, + uint16_t odrivebufflen); uint8_t USBD_CDC_SetRxBuffer (USBD_HandleTypeDef *pdev, - uint8_t *pbuff); + uint8_t *pbuff, uint8_t* odrivebuff); uint8_t USBD_CDC_ReceivePacket (USBD_HandleTypeDef *pdev, uint8_t endpoint_pair); diff --git a/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c b/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c index 2bc01513..6c21e45c 100644 --- a/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c +++ b/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c @@ -292,9 +292,9 @@ __ALIGN_BEGIN uint8_t USBD_CDC_CfgDesc[USB_CDC_CONFIG_DESC_SIZ] __ALIGN_END = 0x0B, /* bDescriptorType: Interface Association Descriptor */ 0x02, /* bFirstInterface */ 0x01, /* bInterfaceCount */ - 0x00, /* bFunctionClass: Communication Interface Class */ - 0x00, /* bFunctionSubClass: Abstract Control Model */ - 0x00, /* bFunctionProtocol: Common AT commands */ + 0x00, /* bFunctionClass: */ + 0x00, /* bFunctionSubClass: */ + 0x00, /* bFunctionProtocol: */ 0x06, /* iFunction */ /*---------------------------------------------------------------------------*/ @@ -414,15 +414,17 @@ static uint8_t USBD_CDC_Init (USBD_HandleTypeDef *pdev, ((USBD_CDC_ItfTypeDef *)pdev->pUserData)->Init(); /* Init Xfer states */ - hcdc->TxState =0; - hcdc->RxState =0; + hcdc->CDC_Tx.State =0; + hcdc->CDC_Rx.State =0; + hcdc->ODRIVE_Tx.State =0; + hcdc->ODRIVE_Rx.State =0; if(pdev->dev_speed == USBD_SPEED_HIGH ) { /* Prepare Out endpoint to receive next packet */ USBD_LL_PrepareReceive(pdev, CDC_OUT_EP, - hcdc->RxBuffer, + hcdc->CDC_Rx.Buffer, CDC_DATA_HS_OUT_PACKET_SIZE); } else @@ -430,14 +432,14 @@ static uint8_t USBD_CDC_Init (USBD_HandleTypeDef *pdev, /* Prepare Out endpoint to receive next packet */ USBD_LL_PrepareReceive(pdev, CDC_OUT_EP, - hcdc->RxBuffer, + hcdc->CDC_Rx.Buffer, CDC_DATA_FS_OUT_PACKET_SIZE); } /* Prepare ODrive Out endpoint to receive next packet */ USBD_LL_PrepareReceive(pdev, ODRIVE_OUT_EP, - hcdc->RxBuffer, + hcdc->ODRIVE_Rx.Buffer, CDC_DATA_FS_OUT_PACKET_SIZE); } return ret; @@ -568,8 +570,10 @@ static uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum) if(pdev->pClassData != NULL) { - - hcdc->TxState = 0; + if (epnum == CDC_IN_EP) + hcdc->CDC_Tx.State = 0; + if (epnum == ODRIVE_IN_EP) + hcdc->ODRIVE_Tx.State = 0; osSemaphoreRelease(sem_usb_tx); return USBD_OK; } @@ -710,12 +714,16 @@ uint8_t USBD_CDC_RegisterInterface (USBD_HandleTypeDef *pdev, */ uint8_t USBD_CDC_SetTxBuffer (USBD_HandleTypeDef *pdev, uint8_t *pbuff, - uint16_t length) + uint16_t length, + uint8_t* odrivebuff, + uint16_t odrivebufflen) { USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; - hcdc->TxBuffer = pbuff; - hcdc->TxLength = length; + hcdc->CDC_Tx.Buffer = pbuff; + hcdc->CDC_Tx.Length = length; + hcdc->ODRIVE_Tx.Buffer = odrivebuff; + hcdc->ODRIVE_Tx.Length = odrivebufflen; return USBD_OK; } @@ -728,11 +736,12 @@ uint8_t USBD_CDC_SetTxBuffer (USBD_HandleTypeDef *pdev, * @retval status */ uint8_t USBD_CDC_SetRxBuffer (USBD_HandleTypeDef *pdev, - uint8_t *pbuff) + uint8_t *pbuff, uint8_t* odrivebuff) { USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; - hcdc->RxBuffer = pbuff; + hcdc->CDC_Rx.Buffer = pbuff; + hcdc->ODRIVE_Rx.Buffer = odrivebuff; return USBD_OK; } @@ -750,25 +759,30 @@ uint8_t USBD_CDC_TransmitPacket(USBD_HandleTypeDef *pdev, uint8_t endpoint_pair if(pdev->pClassData != NULL) { - if(hcdc->TxState == 0) + // Select Endpoint + USBD_CDC_EP_HandleTypeDef* hEP_Tx; + uint8_t in_ep; + if (endpoint_pair == CDC_OUT_EP) { + hEP_Tx = hcdc->CDC_Tx; + in_ep = CDC_IN_EP; + } else if (endpoint_pair == ODRIVE_OUT_EP) { + hEP_Tx = hcdc->ODRIVE_Tx; + in_ep = ODRIVE_IN_EP; + } else { + return USBD_FAIL; + } + + if(hEP_Tx->State == 0) { /* Tx Transfer in progress */ - hcdc->TxState = 1; - - //endpoint_pair = 1; - if (endpoint_pair == 1) { - /* Transmit next packet */ - USBD_LL_Transmit(pdev, - CDC_IN_EP, - hcdc->TxBuffer, - hcdc->TxLength); - } else if (endpoint_pair == 3) { - USBD_LL_Transmit(pdev, - ODRIVE_IN_EP, - hcdc->TxBuffer, - hcdc->TxLength); - } + hEP_Tx->State = 1; + /* Transmit next packet */ + USBD_LL_Transmit(pdev, + in_ep, + hEP_Tx->Buffer, + hEP_Tx->Length); + return USBD_OK; } else diff --git a/Firmware/Board/v3/Src/usbd_cdc_if.c b/Firmware/Board/v3/Src/usbd_cdc_if.c index 77e70b2c..bcf1482d 100644 --- a/Firmware/Board/v3/Src/usbd_cdc_if.c +++ b/Firmware/Board/v3/Src/usbd_cdc_if.c @@ -117,10 +117,12 @@ /* Create buffer for reception and transmission */ /* It's up to user to redefine and/or remove those define */ /** Received data over USB are stored in this buffer */ -uint8_t UserRxBufferFS[APP_RX_DATA_SIZE]; +uint8_t CDCRxBufferFS[APP_RX_DATA_SIZE]; +uint8_t ODRIVERxBufferFS[APP_RX_DATA_SIZE]; /** Data to send over USB CDC are stored in this buffer */ -uint8_t UserTxBufferFS[APP_TX_DATA_SIZE]; +uint8_t CDCTxBufferFS[APP_TX_DATA_SIZE]; +uint8_t ODRIVETxBufferFS[APP_TX_DATA_SIZE]; /* USER CODE BEGIN PRIVATE_VARIABLES */ /* USER CODE END PRIVATE_VARIABLES */ @@ -315,14 +317,29 @@ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len, uint8_t endpoint_pair) //Check length if (Len > USB_TX_DATA_SIZE) return USBD_FAIL; - // Check for ongoing transmission + USBD_CDC_HandleTypeDef* hcdc = (USBD_CDC_HandleTypeDef*) hUsbDeviceFS.pClassData; - if (hcdc->TxState != 0) - return USBD_BUSY; + + // Select EP + USBD_CDC_EP_HandleTypeDef* hEP_Tx; + uint8_t* TxBuff; + if (endpoint_pair == CDC_OUT_EP) { + hEP_Tx = hcdc->CDC_Tx; + TxBuff = CDCTxBufferFS; + } else if (endpoint_pair == ODRIVE_OUT_EP) { + hEP_Tx = hcdc->ODRIVE_Tx; + TxBuff = ODRIVETxBufferFS; + } else { + return USBD_FAIL; + } + + // Check for ongoing transmission + if (hEP_Tx->State != 0) + return USBD_BUSY; // memcpy Buf into UserTxBufferFS - memcpy(UserTxBufferFS, Buf, Len); + memcpy(TxBuff, Buf, Len); // Update Len - USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, Len); + USBD_CDC_SetTxBuffer(&hUsbDeviceFS, TxBuff, Len); result = USBD_CDC_TransmitPacket(&hUsbDeviceFS, endpoint_pair); /* USER CODE END 7 */ return result; From 94d06cee95de36c0c90fc0a7070301f5ab929913 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Sat, 1 Sep 2018 00:09:28 -0700 Subject: [PATCH 22/41] Update getting-started.md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index ddb42665..6d442a55 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -175,7 +175,7 @@ For instance, to set the current limit of M0 to 10A you would type: `odrv0.axis0 ### 2. Set other hardware parameters: - * `odrv0.config.brake_resistance` [Ohm]: This is the resistance of the brake resistor. If you are not using it, you may set it to `0`. + * `odrv0.config.brake_resistance` [Ohm]: This is the resistance of the brake resistor. If you are not using it, you may set it to `0`. Note that there may be some extra resistance in your wiring and in the screw terminals, so if you are getting issues while braking you may want to increase this parameter by around 0.05 ohm. * `odrv0.axis0.motor.config.pole_pairs`: This is the number of **magnet poles** in the rotor, **divided by two**. You can simply count the number of permanent magnets in the rotor, if you can see them. _Note: this is not the same as the number of coils in the stator._ * `odrv0.axis0.motor.config.motor_type`: This is the type of motor being used. Currently two types of motors are supported: High-current motors (`MOTOR_TYPE_HIGH_CURRENT`) and Gimbal motors (`MOTOR_TYPE_GIMBAL`). From c2cd26cc6ef3befc08452bf373f1ba87e529bcf5 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Sat, 1 Sep 2018 19:29:49 -0700 Subject: [PATCH 23/41] change deadline style timeout for regular timeouts in USB and UART interfaces --- Firmware/communication/interface_uart.cpp | 7 ++++--- Firmware/communication/interface_usb.cpp | 11 ++++++----- tools/odrive/utils.py | 9 +++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Firmware/communication/interface_uart.cpp b/Firmware/communication/interface_uart.cpp index 9eee9a0d..b3141138 100644 --- a/Firmware/communication/interface_uart.cpp +++ b/Firmware/communication/interface_uart.cpp @@ -19,7 +19,7 @@ static uint8_t dma_rx_buffer[UART_RX_BUFFER_SIZE]; static uint32_t dma_last_rcv_idx; // FIXME: the stdlib doesn't know about CMSIS threads, so this is just a global variable -static thread_local uint32_t deadline_ms = 0; +// static thread_local uint32_t deadline_ms = 0; osThreadId uart_thread; @@ -32,7 +32,8 @@ public: size_t chunk = length < UART_TX_BUFFER_SIZE ? length : UART_TX_BUFFER_SIZE; // wait for USB interface to become ready // TODO: implement ring buffer to get a more continuous stream of data - if (osSemaphoreWait(sem_uart_dma, deadline_to_timeout(deadline_ms)) != osOK) + // if (osSemaphoreWait(sem_uart_dma, deadline_to_timeout(deadline_ms)) != osOK) + if (osSemaphoreWait(sem_uart_dma, PROTOCOL_SERVER_TIMEOUT_MS) != osOK) return -1; // transmit chunk memcpy(tx_buf_, buffer, chunk); @@ -68,7 +69,7 @@ static void uart_server_thread(void * ctx) { // Fetch the circular buffer "write pointer", where it would write next uint32_t new_rcv_idx = UART_RX_BUFFER_SIZE - huart4.hdmarx->Instance->NDTR; - deadline_ms = timeout_to_deadline(PROTOCOL_SERVER_TIMEOUT_MS); + // deadline_ms = timeout_to_deadline(PROTOCOL_SERVER_TIMEOUT_MS); // Process bytes in one or two chunks (two in case there was a wrap) if (new_rcv_idx < dma_last_rcv_idx) { uart4_stream_input.process_bytes(dma_rx_buffer + dma_last_rcv_idx, diff --git a/Firmware/communication/interface_usb.cpp b/Firmware/communication/interface_usb.cpp index b864981e..0bff5d72 100644 --- a/Firmware/communication/interface_usb.cpp +++ b/Firmware/communication/interface_usb.cpp @@ -18,7 +18,7 @@ static uint32_t usb_len; static uint8_t active_endpoint_pair; // FIXME: the stdlib doesn't know about CMSIS threads, so this is just a global variable -static thread_local uint32_t deadline_ms = 0; +// static thread_local uint32_t deadline_ms = 0; osThreadId usb_thread; @@ -31,7 +31,8 @@ public: if (length > USB_TX_DATA_SIZE) return -1; // wait for USB interface to become ready - if (osSemaphoreWait(sem_usb_tx, deadline_to_timeout(deadline_ms)) != osOK) { + // if (osSemaphoreWait(sem_usb_tx, deadline_to_timeout(deadline_ms)) != osOK) { + if (osSemaphoreWait(sem_usb_tx, PROTOCOL_SERVER_TIMEOUT_MS) != osOK) { // If the host resets the device it might be that the TX-complete handler is never called // and the sem_usb_tx semaphore is never released. To handle this we just override the // TX buffer if this wait times out. The implication is that the channel is no longer lossless. @@ -85,11 +86,11 @@ static void usb_server_thread(void * ctx) { (void) ctx; for (;;) { - const uint32_t usb_check_timeout = 1; // ms - osStatus sem_stat = osSemaphoreWait(sem_usb_rx, usb_check_timeout); + // const uint32_t usb_check_timeout = 1; // ms + osStatus sem_stat = osSemaphoreWait(sem_usb_rx, osWaitForever); if (sem_stat == osOK) { usb_stats_.rx_cnt++; - deadline_ms = timeout_to_deadline(PROTOCOL_SERVER_TIMEOUT_MS); + // deadline_ms = timeout_to_deadline(PROTOCOL_SERVER_TIMEOUT_MS); if (active_endpoint_pair == CDC_OUT_EP && board_config.enable_ascii_protocol_on_usb) { ASCII_protocol_parse_stream(usb_buf, usb_len, usb_stream_output); } else { diff --git a/tools/odrive/utils.py b/tools/odrive/utils.py index 45249262..1b6c3089 100755 --- a/tools/odrive/utils.py +++ b/tools/odrive/utils.py @@ -109,8 +109,8 @@ def rate_test(device): Tests how many integers per second can be transmitted """ - import matplotlib.pyplot as plt - plt.ion() + # import matplotlib.pyplot as plt + # plt.ion() print("reading 10000 values...") numFrames = 10000 @@ -118,13 +118,14 @@ def rate_test(device): for _ in range(numFrames): vals.append(device.axis0.loop_counter) - plt.plot(vals) - loopsPerFrame = (vals[-1] - vals[0])/numFrames loopsPerSec = (168000000/(2*10192)) FramePerSec = loopsPerSec/loopsPerFrame print("Frames per second: " + str(FramePerSec)) + # plt.plot(vals) + # plt.show(block=True) + def usb_burn_in_test(get_var_callback, cancellation_token): """ Starts background threads that read a values form the USB device in a spin-loop From cd64efb664a3a06f785d7904a67f920b1409e5b0 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Sun, 2 Sep 2018 15:35:11 -0700 Subject: [PATCH 24/41] Update configuring-vscode.md --- docs/configuring-vscode.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/configuring-vscode.md b/docs/configuring-vscode.md index 70212be0..17f7e702 100644 --- a/docs/configuring-vscode.md +++ b/docs/configuring-vscode.md @@ -11,10 +11,8 @@ Before doing the VSCode setup, make sure you've installed all of your [prerequis 1. Install extensions. This can be done directly from VSCode (Ctrl+Shift+X) * Required extensions: * C/C++ - * Recommended Extensions: * Cortex-Debug - * vscode-icons - * Code Outline + * Recommended Extensions: * Include Autocomplete * Path Autocomplete * Auto Comment Blocks @@ -51,4 +49,4 @@ Note: If developing on Windows, you should have `arm-none-eabi-gdb` and `openOCD ## Cleaning the Build This sometimes needs to be done if you change branches. -* Open a terminal (View -> Integrated Terminal) and enter `make clean` \ No newline at end of file +* Open a terminal (View -> Integrated Terminal) and enter `make clean` From e26e93212cadce2f7a0104f46b5bd133fe1128dd Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 7 Sep 2018 19:44:00 -0700 Subject: [PATCH 25/41] first compile of new buffers --- Firmware/.vscode/c_cpp_properties.json | 1 + .../Class/CDC/Inc/usbd_cdc.h | 5 +- .../Class/CDC/Src/usbd_cdc.c | 82 ++++++++----- Firmware/Board/v3/Src/usbd_cdc_if.c | 14 ++- Firmware/MotorControl/controller.hpp | 7 +- Firmware/communication/interface_usb.cpp | 113 +++++++++++++----- Firmware/communication/interface_usb.h | 2 +- 7 files changed, 154 insertions(+), 70 deletions(-) diff --git a/Firmware/.vscode/c_cpp_properties.json b/Firmware/.vscode/c_cpp_properties.json index c77d5165..cb9d040e 100644 --- a/Firmware/.vscode/c_cpp_properties.json +++ b/Firmware/.vscode/c_cpp_properties.json @@ -29,6 +29,7 @@ "HW_VERSION_MAJOR=3", "HW_VERSION_MINOR=5", "HW_VERSION_VOLTAGE=24", + "USB_PROTOCOL_NATIVE", "__weak=\"__attribute__((weak))\"", "__packed=\"__attribute__((__packed__))\"", "__GNUC__" diff --git a/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h b/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h index 3f250554..c029e22a 100644 --- a/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h +++ b/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h @@ -160,11 +160,10 @@ uint8_t USBD_CDC_RegisterInterface (USBD_HandleTypeDef *pdev, uint8_t USBD_CDC_SetTxBuffer (USBD_HandleTypeDef *pdev, uint8_t *pbuff, uint16_t length, - uint8_t* odrivebuff, - uint16_t odrivebufflen); + uint8_t endpoint_pair); uint8_t USBD_CDC_SetRxBuffer (USBD_HandleTypeDef *pdev, - uint8_t *pbuff, uint8_t* odrivebuff); + uint8_t *pbuff, uint8_t endpoint_pair); uint8_t USBD_CDC_ReceivePacket (USBD_HandleTypeDef *pdev, uint8_t endpoint_pair); diff --git a/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c b/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c index 6c21e45c..631bb392 100644 --- a/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c +++ b/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c @@ -574,6 +574,7 @@ static uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum) hcdc->CDC_Tx.State = 0; if (epnum == ODRIVE_IN_EP) hcdc->ODRIVE_Tx.State = 0; + //Note: We could use independent semaphores for simoultainous USB transmission. osSemaphoreRelease(sem_usb_tx); return USBD_OK; } @@ -593,15 +594,24 @@ static uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum) static uint8_t USBD_CDC_DataOut (USBD_HandleTypeDef *pdev, uint8_t epnum) { USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; + + USBD_CDC_EP_HandleTypeDef* hEP_Rx; + if (epnum == CDC_OUT_EP) { + hEP_Rx = &hcdc->CDC_Rx; + } else if (epnum == ODRIVE_OUT_EP) { + hEP_Rx = &hcdc->ODRIVE_Rx; + } else { + return USBD_FAIL; + } /* Get the received data length */ - hcdc->RxLength = USBD_LL_GetRxDataSize (pdev, epnum); + hEP_Rx->Length = USBD_LL_GetRxDataSize (pdev, epnum); /* USB data will be immediately processed, this allow next USB traffic being NAKed till the end of the application Xfer */ if(pdev->pClassData != NULL) { - ((USBD_CDC_ItfTypeDef *)pdev->pUserData)->Receive(hcdc->RxBuffer, &hcdc->RxLength, epnum); + ((USBD_CDC_ItfTypeDef *)pdev->pUserData)->Receive(hEP_Rx->Buffer, &hEP_Rx->Length, epnum); return USBD_OK; } @@ -715,15 +725,21 @@ uint8_t USBD_CDC_RegisterInterface (USBD_HandleTypeDef *pdev, uint8_t USBD_CDC_SetTxBuffer (USBD_HandleTypeDef *pdev, uint8_t *pbuff, uint16_t length, - uint8_t* odrivebuff, - uint16_t odrivebufflen) + uint8_t endpoint_pair) { USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; - hcdc->CDC_Tx.Buffer = pbuff; - hcdc->CDC_Tx.Length = length; - hcdc->ODRIVE_Tx.Buffer = odrivebuff; - hcdc->ODRIVE_Tx.Length = odrivebufflen; + USBD_CDC_EP_HandleTypeDef* hEP_Tx; + if (endpoint_pair == CDC_OUT_EP) { + hEP_Tx = &hcdc->CDC_Tx; + } else if (endpoint_pair == ODRIVE_OUT_EP) { + hEP_Tx = &hcdc->ODRIVE_Tx; + } else { + return USBD_FAIL; + } + + hEP_Tx->Buffer = pbuff; + hEP_Tx->Length = length; return USBD_OK; } @@ -736,12 +752,20 @@ uint8_t USBD_CDC_SetTxBuffer (USBD_HandleTypeDef *pdev, * @retval status */ uint8_t USBD_CDC_SetRxBuffer (USBD_HandleTypeDef *pdev, - uint8_t *pbuff, uint8_t* odrivebuff) + uint8_t *pbuff, uint8_t endpoint_pair) { USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; + + USBD_CDC_EP_HandleTypeDef* hEP_Rx; + if (endpoint_pair == CDC_OUT_EP) { + hEP_Rx = &hcdc->CDC_Rx; + } else if (endpoint_pair == ODRIVE_OUT_EP) { + hEP_Rx = &hcdc->ODRIVE_Rx; + } else { + return USBD_FAIL; + } - hcdc->CDC_Rx.Buffer = pbuff; - hcdc->ODRIVE_Rx.Buffer = odrivebuff; + hEP_Rx->Buffer = pbuff; return USBD_OK; } @@ -763,10 +787,10 @@ uint8_t USBD_CDC_TransmitPacket(USBD_HandleTypeDef *pdev, uint8_t endpoint_pair USBD_CDC_EP_HandleTypeDef* hEP_Tx; uint8_t in_ep; if (endpoint_pair == CDC_OUT_EP) { - hEP_Tx = hcdc->CDC_Tx; + hEP_Tx = &hcdc->CDC_Tx; in_ep = CDC_IN_EP; } else if (endpoint_pair == ODRIVE_OUT_EP) { - hEP_Tx = hcdc->ODRIVE_Tx; + hEP_Tx = &hcdc->ODRIVE_Tx; in_ep = ODRIVE_IN_EP; } else { return USBD_FAIL; @@ -810,23 +834,25 @@ uint8_t USBD_CDC_ReceivePacket(USBD_HandleTypeDef *pdev, uint8_t endpoint_pair) /* Suspend or Resume USB Out process */ if(pdev->pClassData != NULL) { - if (endpoint_pair == CDC_OUT_EP) - { - /* Prepare Out endpoint to receive next packet */ - USBD_LL_PrepareReceive(pdev, - CDC_OUT_EP, - hcdc->RxBuffer, - pdev->dev_speed == USBD_SPEED_HIGH ? CDC_DATA_HS_OUT_PACKET_SIZE : CDC_DATA_FS_OUT_PACKET_SIZE); - } - else if (endpoint_pair == ODRIVE_OUT_EP) - { - /* Prepare ODrive Out endpoint to receive next packet */ - USBD_LL_PrepareReceive(pdev, - ODRIVE_OUT_EP, - hcdc->RxBuffer, - pdev->dev_speed == USBD_SPEED_HIGH ? CDC_DATA_HS_OUT_PACKET_SIZE : CDC_DATA_FS_OUT_PACKET_SIZE); + // Select Endpoint + USBD_CDC_EP_HandleTypeDef* hEP_Rx; + uint8_t out_ep; + if (endpoint_pair == CDC_OUT_EP) { + hEP_Rx = &hcdc->CDC_Rx; + out_ep = CDC_OUT_EP; + } else if (endpoint_pair == ODRIVE_OUT_EP) { + hEP_Rx = &hcdc->ODRIVE_Rx; + out_ep = ODRIVE_OUT_EP; + } else { + return USBD_FAIL; } + /* Prepare Out endpoint to receive next packet */ + USBD_LL_PrepareReceive(pdev, + out_ep, + hEP_Rx->Buffer, + pdev->dev_speed == USBD_SPEED_HIGH ? CDC_DATA_HS_OUT_PACKET_SIZE : CDC_DATA_FS_OUT_PACKET_SIZE); + return USBD_OK; } else diff --git a/Firmware/Board/v3/Src/usbd_cdc_if.c b/Firmware/Board/v3/Src/usbd_cdc_if.c index bcf1482d..b8cb6e6c 100644 --- a/Firmware/Board/v3/Src/usbd_cdc_if.c +++ b/Firmware/Board/v3/Src/usbd_cdc_if.c @@ -179,8 +179,10 @@ static int8_t CDC_Init_FS(void) { /* USER CODE BEGIN 3 */ /* Set Application Buffers */ - USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0); - USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS); + USBD_CDC_SetTxBuffer(&hUsbDeviceFS, CDCTxBufferFS, 0, CDC_OUT_EP); + USBD_CDC_SetRxBuffer(&hUsbDeviceFS, CDCRxBufferFS, CDC_OUT_EP); + USBD_CDC_SetTxBuffer(&hUsbDeviceFS, ODRIVETxBufferFS, 0, ODRIVE_OUT_EP); + USBD_CDC_SetRxBuffer(&hUsbDeviceFS, ODRIVERxBufferFS, ODRIVE_OUT_EP); return (USBD_OK); /* USER CODE END 3 */ } @@ -292,7 +294,7 @@ static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len, uint8_t endpoint_pair) { /* USER CODE BEGIN 6 */ - usb_process_packet(Buf, *Len, endpoint_pair); + usb_rx_process_packet(Buf, *Len, endpoint_pair); return (USBD_OK); /* USER CODE END 6 */ @@ -324,10 +326,10 @@ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len, uint8_t endpoint_pair) USBD_CDC_EP_HandleTypeDef* hEP_Tx; uint8_t* TxBuff; if (endpoint_pair == CDC_OUT_EP) { - hEP_Tx = hcdc->CDC_Tx; + hEP_Tx = &hcdc->CDC_Tx; TxBuff = CDCTxBufferFS; } else if (endpoint_pair == ODRIVE_OUT_EP) { - hEP_Tx = hcdc->ODRIVE_Tx; + hEP_Tx = &hcdc->ODRIVE_Tx; TxBuff = ODRIVETxBufferFS; } else { return USBD_FAIL; @@ -339,7 +341,7 @@ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len, uint8_t endpoint_pair) // memcpy Buf into UserTxBufferFS memcpy(TxBuff, Buf, Len); // Update Len - USBD_CDC_SetTxBuffer(&hUsbDeviceFS, TxBuff, Len); + USBD_CDC_SetTxBuffer(&hUsbDeviceFS, TxBuff, Len, endpoint_pair); result = USBD_CDC_TransmitPacket(&hUsbDeviceFS, endpoint_pair); /* USER CODE END 7 */ return result; diff --git a/Firmware/MotorControl/controller.hpp b/Firmware/MotorControl/controller.hpp index f10b6211..80f5d25a 100644 --- a/Firmware/MotorControl/controller.hpp +++ b/Firmware/MotorControl/controller.hpp @@ -86,12 +86,9 @@ public: make_protocol_property("vel_limit", &config_.vel_limit) ), make_protocol_function("set_pos_setpoint", *this, &Controller::set_pos_setpoint, - "pos_setpoint", - "vel_feed_forward", - "current_feed_forward"), + "pos_setpoint", "vel_feed_forward", "current_feed_forward"), make_protocol_function("set_vel_setpoint", *this, &Controller::set_vel_setpoint, - "vel_setpoint", - "current_feed_forward"), + "vel_setpoint", "current_feed_forward"), make_protocol_function("set_current_setpoint", *this, &Controller::set_current_setpoint, "current_setpoint"), make_protocol_function("start_anticogging_calibration", *this, &Controller::start_anticogging_calibration) diff --git a/Firmware/communication/interface_usb.cpp b/Firmware/communication/interface_usb.cpp index 0bff5d72..ba1decc1 100644 --- a/Firmware/communication/interface_usb.cpp +++ b/Firmware/communication/interface_usb.cpp @@ -13,28 +13,22 @@ #include -static uint8_t* usb_buf; -static uint32_t usb_len; -static uint8_t active_endpoint_pair; - -// FIXME: the stdlib doesn't know about CMSIS threads, so this is just a global variable -// static thread_local uint32_t deadline_ms = 0; - osThreadId usb_thread; - USBStats_t usb_stats_ = {0}; class USBSender : public PacketSink { public: + USBSender(uint8_t endpoint_pair, osSemaphoreId sem_usb_tx) + : endpoint_pair_(endpoint_pair), sem_usb_tx_(sem_usb_tx) {} + int process_packet(const uint8_t* buffer, size_t length) { // cannot send partial packets if (length > USB_TX_DATA_SIZE) return -1; // wait for USB interface to become ready - // if (osSemaphoreWait(sem_usb_tx, deadline_to_timeout(deadline_ms)) != osOK) { - if (osSemaphoreWait(sem_usb_tx, PROTOCOL_SERVER_TIMEOUT_MS) != osOK) { + if (osSemaphoreWait(sem_usb_tx_, PROTOCOL_SERVER_TIMEOUT_MS) != osOK) { // If the host resets the device it might be that the TX-complete handler is never called - // and the sem_usb_tx semaphore is never released. To handle this we just override the + // and the sem_usb_tx_ semaphore is never released. To handle this we just override the // TX buffer if this wait times out. The implication is that the channel is no longer lossless. // TODO: handle endpoint reset properly usb_stats_.tx_overrun_cnt++; @@ -42,15 +36,22 @@ public: // transmit packet uint8_t status = CDC_Transmit_FS( const_cast(buffer) /* casting this const away is safe because... - well... it's not actually. Stupid STM. */, length, active_endpoint_pair); + well... it's not actually. Stupid STM. */, length, endpoint_pair_); if (status != USBD_OK) { - osSemaphoreRelease(sem_usb_tx); + osSemaphoreRelease(sem_usb_tx_); return -1; } usb_stats_.tx_cnt++; return 0; } -} usb_packet_output; +private: + uint8_t endpoint_pair_; + osSemaphoreId sem_usb_tx_; +}; + +// Note we could have independent semaphores here to allow concurrent transmission +USBSender usb_packet_output_cdc(CDC_OUT_EP, sem_usb_tx); +USBSender usb_packet_output_native(ODRIVE_OUT_EP, sem_usb_tx); class TreatPacketSinkAsStreamSink : public StreamSink { public: @@ -71,17 +72,48 @@ public: size_t get_free_space() { return SIZE_MAX; } private: PacketSink& output_; -} usb_stream_output(usb_packet_output); +} usb_stream_output(usb_packet_output_cdc); + +// This is used by the printf feature. Hence the above statics, and below seemingly random ptr (it's externed) +// TODO: less spaghetti code StreamSink* usb_stream_output_ptr = &usb_stream_output; #if defined(USB_PROTOCOL_NATIVE) -BidirectionalPacketBasedChannel usb_channel(usb_packet_output); +BidirectionalPacketBasedChannel usb_channel(usb_packet_output_native); #elif defined(USB_PROTOCOL_NATIVE_STREAM_BASED) StreamBasedPacketSink usb_packetized_output(usb_stream_output); BidirectionalPacketBasedChannel usb_channel(usb_packetized_output); StreamToPacketSegmenter usb_native_stream_input(usb_channel); #endif +struct USBInterface { + uint8_t* rx_buf = nullptr; + uint32_t rx_len = 0; + bool data_pending = false; + uint8_t out_ep; + uint8_t in_ep; + USBSender& usb_sender; +}; + +// Note: statics make this less modular. +// Note: we use a single rx semaphore and loop over data_pending to allow a single pump loop thread +static USBInterface CDC_interface = { + .rx_buf = nullptr, + .rx_len = 0, + .data_pending = false, + .out_ep = CDC_OUT_EP, + .in_ep = CDC_IN_EP, + .usb_sender = usb_packet_output_cdc, +}; +static USBInterface ODrive_interface = { + .rx_buf = nullptr, + .rx_len = 0, + .data_pending = false, + .out_ep = ODRIVE_OUT_EP, + .in_ep = ODRIVE_IN_EP, + .usb_sender = usb_packet_output_native, +}; + static void usb_server_thread(void * ctx) { (void) ctx; @@ -90,27 +122,54 @@ static void usb_server_thread(void * ctx) { osStatus sem_stat = osSemaphoreWait(sem_usb_rx, osWaitForever); if (sem_stat == osOK) { usb_stats_.rx_cnt++; - // deadline_ms = timeout_to_deadline(PROTOCOL_SERVER_TIMEOUT_MS); - if (active_endpoint_pair == CDC_OUT_EP && board_config.enable_ascii_protocol_on_usb) { - ASCII_protocol_parse_stream(usb_buf, usb_len, usb_stream_output); - } else { + + // CDC Interface + if (CDC_interface.data_pending) { + if (board_config.enable_ascii_protocol_on_usb) { + ASCII_protocol_parse_stream(CDC_interface.rx_buf, + CDC_interface.rx_len, usb_stream_output); + } else { #if defined(USB_PROTOCOL_NATIVE) - usb_channel.process_packet(usb_buf, usb_len); + usb_channel.process_packet(CDC_interface.rx_buf, CDC_interface.rx_len); #elif defined(USB_PROTOCOL_NATIVE_STREAM_BASED) - usb_native_stream_input.process_bytes(usb_buf, usb_len, nullptr); + usb_native_stream_input.process_bytes( + CDC_interface.rx_buf, CDC_interface.rx_len, nullptr); #endif + } + USBD_CDC_ReceivePacket(&hUsbDeviceFS, CDC_interface.out_ep); // Allow next packet + } + + // Native Interface + if (ODrive_interface.data_pending) { +#if defined(USB_PROTOCOL_NATIVE) + usb_channel.process_packet(ODrive_interface.rx_buf, ODrive_interface.rx_len); +#elif defined(USB_PROTOCOL_NATIVE_STREAM_BASED) + usb_native_stream_input.process_bytes( + ODrive_interface.rx_buf, ODrive_interface.rx_len, nullptr); +#endif + USBD_CDC_ReceivePacket(&hUsbDeviceFS, ODrive_interface.out_ep); // Allow next packet } - USBD_CDC_ReceivePacket(&hUsbDeviceFS, active_endpoint_pair); // Allow next packet } } } // Called from CDC_Receive_FS callback function, this allows the communication // thread to handle the incoming data -void usb_process_packet(uint8_t *buf, uint32_t len, uint8_t endpoint_pair) { - usb_buf = buf; - usb_len = len; - active_endpoint_pair = endpoint_pair; +void usb_rx_process_packet(uint8_t *buf, uint32_t len, uint8_t endpoint_pair) { + USBInterface* usb_iface; + if (endpoint_pair == CDC_interface.out_ep) { + usb_iface = &CDC_interface; + } else if (endpoint_pair == ODrive_interface.out_ep) { + usb_iface = &ODrive_interface; + } else { + return; + } + + // We don't allow the next USB packet until the previous one has been processed completely. + // Therefore it's safe to write to these vars directly since we know previous processing is complete. + usb_iface->rx_buf = buf; + usb_iface->rx_len = len; + usb_iface->data_pending = true; osSemaphoreRelease(sem_usb_rx); } diff --git a/Firmware/communication/interface_usb.h b/Firmware/communication/interface_usb.h index c4dabb99..0a5b94ff 100644 --- a/Firmware/communication/interface_usb.h +++ b/Firmware/communication/interface_usb.h @@ -21,7 +21,7 @@ typedef struct { extern USBStats_t usb_stats_; -void usb_process_packet(uint8_t *buf, uint32_t len, uint8_t endpoint_pair); +void usb_rx_process_packet(uint8_t *buf, uint32_t len, uint8_t endpoint_pair); void start_usb_server(void); #ifdef __cplusplus From a7ca3a7cbd04c6c8ec7f6ee37e6601448c37c1c6 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 7 Sep 2018 20:56:36 -0700 Subject: [PATCH 26/41] fix one of my bugs, and one of STM --- .../ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c | 5 +++-- Firmware/communication/interface_usb.cpp | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c b/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c index 631bb392..73313522 100644 --- a/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c +++ b/Firmware/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c @@ -570,9 +570,10 @@ static uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum) if(pdev->pClassData != NULL) { - if (epnum == CDC_IN_EP) + // NOTE: We would logically expect xx_IN_EP here, but we actually get the xx_OUT_EP + if (epnum == CDC_OUT_EP) hcdc->CDC_Tx.State = 0; - if (epnum == ODRIVE_IN_EP) + if (epnum == ODRIVE_OUT_EP) hcdc->ODRIVE_Tx.State = 0; //Note: We could use independent semaphores for simoultainous USB transmission. osSemaphoreRelease(sem_usb_tx); diff --git a/Firmware/communication/interface_usb.cpp b/Firmware/communication/interface_usb.cpp index ba1decc1..d6e38948 100644 --- a/Firmware/communication/interface_usb.cpp +++ b/Firmware/communication/interface_usb.cpp @@ -18,7 +18,7 @@ USBStats_t usb_stats_ = {0}; class USBSender : public PacketSink { public: - USBSender(uint8_t endpoint_pair, osSemaphoreId sem_usb_tx) + USBSender(uint8_t endpoint_pair, const osSemaphoreId& sem_usb_tx) : endpoint_pair_(endpoint_pair), sem_usb_tx_(sem_usb_tx) {} int process_packet(const uint8_t* buffer, size_t length) { @@ -46,7 +46,7 @@ public: } private: uint8_t endpoint_pair_; - osSemaphoreId sem_usb_tx_; + const osSemaphoreId& sem_usb_tx_; }; // Note we could have independent semaphores here to allow concurrent transmission From cf33cf2c141440540308a5950e21095fa57aaae4 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 7 Sep 2018 21:11:20 -0700 Subject: [PATCH 27/41] only process each packet once --- Firmware/communication/interface_usb.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Firmware/communication/interface_usb.cpp b/Firmware/communication/interface_usb.cpp index d6e38948..cd44c907 100644 --- a/Firmware/communication/interface_usb.cpp +++ b/Firmware/communication/interface_usb.cpp @@ -125,6 +125,7 @@ static void usb_server_thread(void * ctx) { // CDC Interface if (CDC_interface.data_pending) { + CDC_interface.data_pending = false; if (board_config.enable_ascii_protocol_on_usb) { ASCII_protocol_parse_stream(CDC_interface.rx_buf, CDC_interface.rx_len, usb_stream_output); @@ -141,6 +142,7 @@ static void usb_server_thread(void * ctx) { // Native Interface if (ODrive_interface.data_pending) { + ODrive_interface.data_pending = false; #if defined(USB_PROTOCOL_NATIVE) usb_channel.process_packet(ODrive_interface.rx_buf, ODrive_interface.rx_len); #elif defined(USB_PROTOCOL_NATIVE_STREAM_BASED) From fb5cae841afa5240d2241ef20ce7ab9dcd3bada1 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 7 Sep 2018 21:24:51 -0700 Subject: [PATCH 28/41] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96e8644d..32a214f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Unreleased Features Please add a note of your changes below this heading if you make a Pull Request. +# Unreleased +## Fixed +* Serious reliability issue with USB communication where packets on Native and the CDC interface would collide with each other. + # Releases ## [0.4.3] - 2018-08-30 ### Added From 4a5f2cc1c0095760d844c371fc489d9fd4b05a40 Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Sat, 8 Sep 2018 19:22:51 +1000 Subject: [PATCH 29/41] Additional detail for the control method Added additional detail. --- docs/control.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/control.md b/docs/control.md index 0be73866..3775c033 100644 --- a/docs/control.md +++ b/docs/control.md @@ -4,6 +4,17 @@ The motor controller is a cascaded style position, velocity and current control ![Cascaded pos vel I loops](https://static1.squarespace.com/static/58aff26de4fcb53b5efd2f02/t/5b66284a0e2e72aae8818d64/1533421649405/CascadedController.png?format=2500w) -* The position controller is a P loop with a single proportional gain. -* The velocity controller is a PI loop. -* The current controller is a PI loop. +### Position loop: +The position controller is a P loop with a single proportional gain. +* `vel_cmd = (pos_setpoint - pos_feedback) * pos_gain` + +### Velocity loop: +The velocity controller is a PI loop. +* `vel_error_sum += (vel_cmd - vel_feedback) * vel_integrator_gain` +* `current_cmd = (vel_cmd - vel_feedback) * vel_gain + vel_error_sum + vel_feedforward` +### Current loop: +The current controller is a PI loop. +* `current_error_sum += (current_cmd - current_fb) * current_integrator_gain` +* `voltage_cmd = (current_cmd - current_fb) * current_gain + current_error_sum + current_feedforward` + +For more detail refer to [controller.cpp](https://github.com/madcowswe/ODrive/blob/master/Firmware/MotorControl/controller.cpp#L86). From 5b654192bb1d8344a658c9090f5a8363e4614729 Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Sun, 9 Sep 2018 11:02:58 +1000 Subject: [PATCH 30/41] Moved and updated troubleshooting from wiki --- docs/troubleshooting.md | 95 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 0c0d6cbc..8ae08503 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -21,18 +21,64 @@ The axis error may say that some other component has failed. Say it reports `ERR * Encoder error flags defined [here](../Firmware/MotorControl/encoder.hpp). * Sensorless estimator error flags defined [here](../Firmware/MotorControl/sensorless_estimator.hpp). -## DRV fault +## Common Axis Errors + +* `ERROR_INVALID_STATE = 0x01` + +Typically returned along with another error. Resolve that error and then reboot using `odrv0.reboot()` or remoivng power, waiting 5 seconds and restoring power to return to normal operating. + +* `ERROR_DC_BUS_UNDER_VOLTAGE = 0x02` + +Confirm that your power leads are connected securely. For initial testing a 12V PSU which can supply a couple of amps should be sufficient while the use of low current 'wall wart' plug packs may lead to inconsistent behaviour and is not recommended. + +You can monitor your PUS voltage using liveplotter in odrive tool by entering `start_liveplotter(lambda: [odrv0.vbus_voltage])`. If you see your votlage drop below ~ 8V then you will trip this error. Even a relatively small motor can draw multiple kW momentary and so unless you have a very large PSU or are running of a battery you may encounter this error when executing high speed movements with a high current limit. To limit your PSU power draw you can limit your motor current and/or velocity limit `odrv0.axis0.controller.config.vel_limit` and `odrv0.axis0.motor.config.current_lim`. + +* `ERROR_DC_BUS_OVER_VOLTAGE = 0x04` + +Confirm that you have a break resistor of the correct value connected securly and that `odrv0.config.brake_resistance` is set to the value of your break resistor. + +You can monitor your PUS voltage using liveplotter in odrive tool by entering `start_liveplotter(lambda: [odrv0.vbus_voltage])`. If during a move you see the voltage rise above your PSU's nominal set voltage then you have your break resistance set too low. This may happen if you are using long wires or small gauge wires to connect your break resistor to your odrive which will added extra resistance. This extra resistance needs to be accounted for to prevent this voltage spike. If you have checked all your connections you can also try increasing your break resistance by ~ 0.01 Ohm at a time to a maximum of 0.05 greater than your break resistor value. + +## Common Motor Errors + +* `ERROR_PHASE_RESISTANCE_OUT_OF_RANGE = 0x0001` and `ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE = 0x0002` + +During calibration the motor resistance and [inductance](https://en.wikipedia.org/wiki/Inductance) is measured. If the measured motor resistance or inductance falls outside a set range this error will be returned. Check that all motor leads are connected securely. + +The measured values can be measured using odrivetool as is shown below: +``` +In [2]: odrv0.axis0.motor.config.phase_inductance +Out[2]: 1.408751450071577e-05 + +In [3]: odrv0.axis0.motor.config.phase_resistance +Out[3]: 0.029788672924041748 +``` +Some motors will have a considerably different phase resistance and inductance than this. For example, gimbal motors, some small motors (e.g. < 10A peak current). If you think this applies to you try increasing `odrv0.axis0.motor.config.resistance_calib_max_voltage` from its default value of 1 using odrive tool and repeat the motor calibration process. If your motor has a small peak current draw (e.g. < 20A) you can also try decreasing `odrv0.axis0.motor.config.calibration_current` from its default value of 10A. + +* `ERROR_DRV_FAULT = 0x0008` The ODrive v3.4 is known to have a hardware issue whereby the motors would stop operating when applying high currents to M0. The reported error of both motors in this case is `ERROR_DRV_FAULT`. The conjecture is that the high switching current creates large ripples in the -power supply of the DRV8301 gate driver chips, thus tripping its undervoltage -fault detection. +power supply of the DRV8301 gate driver chips, thus tripping its under-voltage fault detection. -* Limit the M0 current to 40A. The lowest current at which the DRV fault was observed is 45A on one test motor and 50A on another test motor. -* Refer to [this post](https://discourse.odriverobotics.com/t/drv-fault-on-odrive-v3-4/558) for instructions for a hardware fix +To resolve this issue you can limit the M0 current to 40A. The lowest current at which the DRV fault was observed is 45A on one test motor and 50A on another test motor. Refer to [this post](https://discourse.odriverobotics.com/t/drv-fault-on-odrive-v3-4/558) for instructions for a hardware fix + +## Common Encoder Errors + +* `ERROR_CPR_OUT_OF_RANGE = 0x02` + +Confirm you have entered the correct count per rotation (CPR) for [your encoder](https://docs.odriverobotics.com/encoders). Note that the AMT encoders are configurable using the micro-switches on the encoder PCB and so you may need to check that these are in the right positions. If your encoder lists its pulse per rotation (PPR) multiply that number by four to get CPR. + +* `ERROR_NO_RESPONSE = 0x04` + +Confirm that your encoder is plugged into the right pins on the odrive board. + +* `ERROR_INDEX_NOT_FOUND_YET = 0x20` + +Check that your encoder is a model that has an index pulse (will have a wire connected to the pin labelled Z and that it is plugged in correctly. ## USB Connectivity Issues @@ -47,3 +93,42 @@ fault detection. * Run `odrivetools` with the `--verbose` option. * Run `PYUSB_DEBUG=debug odrivetools` to get even more log output. * If you're a developer you can use Wireshark to capture USB traffic. + * Try a different USB cable + * Try routing your USB cable so that it is far away from the motor and PSU cables to reduce EMI + +## **Firmware Issues** + +### Failure to build the firmware when running `make` +- Clear out temporary files from previous compiles by first running `make clean` to prevent conflicts. +- **Windows users**: Confirm that tup has been correctly added to path by running `env|grep PATH` in Git Bash. If you see no mention of tup then you must [add its location to your PATH environment variable.](https://docs.alfresco.com/4.2/tasks/fot-addpath.html). Note that you may need to restart for the added path to take effect. + +### Failure to flash the firmware when running `make flash` +- If using an ST-link, confirm that the ST-link is connected the correct pins and that you have power supplied to the board. This can be by the 5V pin on the ST link or the main DC power jack. No power is supplied over the USB connection. + +## Other issues that may not produce an error code + +### Motor cuts off or spins uncontrollably at high rotational speeds (ie: > 5000 RPM) +- You may be approaching the limit of your encoder. The 2400 count/rotation encoders that were initially included with odrive are realistically limited to around 5000 RPM. Exceeding this speed causes the odrive to lose track of position. This can only be fixed by using an alternative encoder or gearing down the output of your motor onto your encoder so that it still sees < 5000RPM at full speed. If using the gearing options be sure to change your counts/rotation accordingly. + +### Motor vibrates when stationary or makes constant noise + +- Likely due to incorrect gains, specifically `vel_gain` may be set too high. Try following the [tuning procedure](https://docs.odriverobotics.com/commands). +- Check encoder shaft connection. Grub screws may vibrate lose with time. If using a CUI shaft encoder try remounting the plastic retaining ring and confirm that it is not coming into contact with the encoder housing. Also confirm that the encoder is securely mounted. +- If you are using a high resolution encoder (>4000 counts/rotation) then increasing encoder_pll_bandwidth may help reduce vibration. +- If you connect your motor to an object with a large moment of inertia (such as a flywheel) this will help reduce vibrations at high gians. However, make sure that all connections are ridged. Cheap shaft couplers or belts under low tension can introduce enough flex into a system that the motor may still vibrate independently. + +### Motor overshoots target position or oscillates back and forth +- Likely due to incorrect gains for a given motor current limit. Specifically `pos_gain` is set too high. Try following the [tuning procedure](https://docs.odriverobotics.com/commands). +- Increase the current limit of your motor for more torque. + +### Motor slowly starts to increase in speed +- Encoder has likely slipped. This may occur when your motor makes a hard stop or violently vibrates causing something to come lose. Power the board off and on again so that it undertakes a new calibration. If you are using an index search on startup then you will need to repeat the index calibration process. + +### Motor feels like it has less torque than it should and/or gets hot sitting still while under no load. +- Encoder has likely slipped causing the motor controller to commutate the wrong windings slightly which reduces output torque and produces excess heat as the motor 'fights itself'. + +### False steps or direction changes when using step/dir +- Prior to Odrive board V3.5 no filtering is present on the GPIO pins used for step/dir interface and so inductively coupled noise may causes false steps to be detected. Odrive V3.5 and has onboard filtering to resolve this issue. +- If you experience this issue use a twisted pair cable between your microcontroller that’s generating the step/dir signals and your odrive board. A section cut from cat-5 cable works well as does just twisting some normal insulated wire together. +- Ensure that the step/dir signal cables are not draped over the odrive board, are not running in parallel to the motor or power supply cables. +- If the above does not resolve your issue on V3.4 boards and lower try adding a ~22 Ohm resistor in series with the step and direction pins along with a ~ 4.7 nF capacitor between the ground pin and the step and dir pins such as shown [here](https://cdn.discordapp.com/attachments/369667319280173069/420811057431445504/IMG_20180306_211224.jpg). From 438c1b5f169397b76d1566a56665a3d576e56d9b Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Mon, 10 Sep 2018 10:22:57 +1000 Subject: [PATCH 31/41] rephrase some parts --- docs/troubleshooting.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 8ae08503..94f51117 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -45,7 +45,7 @@ You can monitor your PUS voltage using liveplotter in odrive tool by entering `s During calibration the motor resistance and [inductance](https://en.wikipedia.org/wiki/Inductance) is measured. If the measured motor resistance or inductance falls outside a set range this error will be returned. Check that all motor leads are connected securely. -The measured values can be measured using odrivetool as is shown below: +The measured values can be viewed using odrivetool as is shown below: ``` In [2]: odrv0.axis0.motor.config.phase_inductance Out[2]: 1.408751450071577e-05 @@ -64,7 +64,7 @@ is `ERROR_DRV_FAULT`. The conjecture is that the high switching current creates large ripples in the power supply of the DRV8301 gate driver chips, thus tripping its under-voltage fault detection. -To resolve this issue you can limit the M0 current to 40A. The lowest current at which the DRV fault was observed is 45A on one test motor and 50A on another test motor. Refer to [this post](https://discourse.odriverobotics.com/t/drv-fault-on-odrive-v3-4/558) for instructions for a hardware fix +To resolve this issue you can limit the M0 current to 40A. The lowest current at which the DRV fault was observed is 45A on one test motor and 50A on another test motor. Refer to [this post](https://discourse.odriverobotics.com/t/drv-fault-on-odrive-v3-4/558) for instructions for a hardware fix. ## Common Encoder Errors @@ -78,7 +78,7 @@ Confirm that your encoder is plugged into the right pins on the odrive board. * `ERROR_INDEX_NOT_FOUND_YET = 0x20` -Check that your encoder is a model that has an index pulse (will have a wire connected to the pin labelled Z and that it is plugged in correctly. +Check that your encoder is a model that has an index pulse. If your encoder does not have a wire connected to pin Z on your odrive then it does not output an index pulse. ## USB Connectivity Issues From 3310e330ba40332a6de4212af7687c4e0a8c63ad Mon Sep 17 00:00:00 2001 From: Josh Marshall Date: Tue, 11 Sep 2018 15:06:24 +1000 Subject: [PATCH 32/41] Fixed udev rules due to default rules overriding our permissions. --- docs/getting-started.md | 2 +- tools/odrive/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 6d442a55..141311ae 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -123,7 +123,7 @@ Try step 5 again 2. Install the ODrive tools by opening a terminal and typing `pip install odrive` Enter 3. __Linux__: set up USB permissions ```bash - echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="0d[0-9][0-9]", MODE="0666"' | sudo tee /etc/udev/rules.d/50-odrive.rules + echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="0d[0-9][0-9]", MODE="0666"' | sudo tee /etc/udev/rules.d/99-odrive.rules sudo udevadm control --reload-rules sudo udevadm trigger # until you reboot you may need to do this everytime you reset the ODrive ``` diff --git a/tools/odrive/utils.py b/tools/odrive/utils.py index 45249262..26087422 100755 --- a/tools/odrive/utils.py +++ b/tools/odrive/utils.py @@ -151,7 +151,7 @@ def setup_udev_rules(logger): logger.error("This command only makes sense on Linux") if os.getuid() != 0: logger.warn("you should run this as root, otherwise it will probably not work") - with open('/etc/udev/rules.d/50-odrive.rules', 'w') as file: + with open('/etc/udev/rules.d/99-odrive.rules', 'w') as file: file.write('SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="0d3[0-9]", MODE="0666"\n') subprocess.check_call(["udevadm", "control", "--reload-rules"]) subprocess.check_call(["udevadm", "trigger"]) From 8bc59ab9ebcd2b8faaea5e202704b292573e22d4 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Tue, 11 Sep 2018 18:48:55 -0700 Subject: [PATCH 33/41] Update getting-started.md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 141311ae..ccbcc412 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -123,7 +123,7 @@ Try step 5 again 2. Install the ODrive tools by opening a terminal and typing `pip install odrive` Enter 3. __Linux__: set up USB permissions ```bash - echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="0d[0-9][0-9]", MODE="0666"' | sudo tee /etc/udev/rules.d/99-odrive.rules + echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="0d[0-9][0-9]", MODE="0666"' | sudo tee /etc/udev/rules.d/91-odrive.rules sudo udevadm control --reload-rules sudo udevadm trigger # until you reboot you may need to do this everytime you reset the ODrive ``` From c5863dc7cdfb7559ec8e05deafee857d71e2aafd Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Tue, 11 Sep 2018 18:49:10 -0700 Subject: [PATCH 34/41] Update utils.py --- tools/odrive/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/odrive/utils.py b/tools/odrive/utils.py index 26087422..95104d72 100755 --- a/tools/odrive/utils.py +++ b/tools/odrive/utils.py @@ -151,7 +151,7 @@ def setup_udev_rules(logger): logger.error("This command only makes sense on Linux") if os.getuid() != 0: logger.warn("you should run this as root, otherwise it will probably not work") - with open('/etc/udev/rules.d/99-odrive.rules', 'w') as file: + with open('/etc/udev/rules.d/91-odrive.rules', 'w') as file: file.write('SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="0d3[0-9]", MODE="0666"\n') subprocess.check_call(["udevadm", "control", "--reload-rules"]) subprocess.check_call(["udevadm", "trigger"]) From a7bc212e13b7327b0ef1fbef07faa1ae2de6bd61 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Tue, 11 Sep 2018 18:50:04 -0700 Subject: [PATCH 35/41] Update getting-started.md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index ccbcc412..b4ec004d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -125,7 +125,7 @@ Try step 5 again ```bash echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="0d[0-9][0-9]", MODE="0666"' | sudo tee /etc/udev/rules.d/91-odrive.rules sudo udevadm control --reload-rules - sudo udevadm trigger # until you reboot you may need to do this everytime you reset the ODrive + sudo udevadm trigger ``` ## Start `odrivetool` From 3f36e7df743436687d6ec66e92d17fb9d9edf731 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Wed, 12 Sep 2018 01:33:00 -0700 Subject: [PATCH 36/41] make docs TOC consistent --- docs/commands.md | 10 ++++++++++ docs/getting-started.md | 5 ++--- docs/interfaces.md | 11 ++++------- docs/odrivetool.md | 12 ++++++++++++ docs/troubleshooting.md | 15 ++++++++++++++- 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 212a4899..64fe1870 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -2,6 +2,16 @@ We will use the `` as a placeholder for any ODrive object. Every ODrive controller is an ODrive object. In `odrivetool` this is usually `odrv0`. Furthermore we use `` as a placeholder for any axis, which is an attribute of an ODrive object (for example `odrv0.axis0`). An axis represents where the motors are connected. (axis0 for M0 or axis1 for M1) +### Table of contents + + +- [Per-Axis commands](#per-axis-commands) +- [System monitoring commands](#system-monitoring-commands) +- [General system commands](#general-system-commands) +- [Setting up sensorless](#setting-up-sensorless) + + + ## Per-Axis commands For the most part, both axes on the ODrive can be controlled independently. diff --git a/docs/getting-started.md b/docs/getting-started.md index b4ec004d..fd453bbc 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -7,8 +7,7 @@ permalink: / # Getting Started ### Table of contents - - + - [Hardware Requirements](#hardware-requirements) - [Wiring up the ODrive](#wiring-up-the-odrive) @@ -18,7 +17,7 @@ permalink: / - [Position control of M0](#position-control-of-m0) - [What's next?](#whats-next) - + ## Hardware Requirements diff --git a/docs/interfaces.md b/docs/interfaces.md index be759832..ba6ccfb0 100644 --- a/docs/interfaces.md +++ b/docs/interfaces.md @@ -5,19 +5,16 @@ The ODrive can be controlled over various ports and protocols. If you're comfortable with embedded systems development, you can also run custom code directly on the ODrive. For that refer to the [developer documentation](developer-guide.md). ### Table of contents - - + - [Pinout](#pinout) - [Native Protocol](#native-protocol) -- [ASCII Protocol](#ascii-protocol) (and Arduino) +- [ASCII protocol](#ascii-protocol) - [Step/direction](#stepdirection) -- [RC PWM input](#rc-pwm-input) (coming soon) +- [RC PWM input](#rc-pwm-input) - [Ports](#ports) - - [USB](#usb) - - [UART](#uart) - + ## Pinout diff --git a/docs/odrivetool.md b/docs/odrivetool.md index 80d926b7..2356266c 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -2,6 +2,18 @@ The ODrive Tool is the accompanying PC program for the ODrive. It's main purpose is to provide an interactive shell to control the device manually, as well as some supporting functions like firmware update. +### Table of contents + + +- [Installation](#installation) +- [Multiple ODrives](#multiple-odrives) +- [Configuration Backup](#configuration-backup) +- [Device Firmware Update](#device-firmware-update) +- [Flashing with an STLink](#flashing-with-an-stlink) +- [Liveplotter](#liveplotter) + + + ## Installation Refer to the [Getting Started guide](getting-started#downloading-and-installing-tools). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 94f51117..b6d36720 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,5 +1,18 @@ # Troubleshooting +Table of Contents: + + +- [Error codes](#error-codes) +- [Common Axis Errors](#common-axis-errors) +- [Common Motor Errors](#common-motor-errors) +- [Common Encoder Errors](#common-encoder-errors) +- [USB Connectivity Issues](#usb-connectivity-issues) +- [Firmware Issues](#firmware-issues) +- [Other issues that may not produce an error code](#other-issues-that-may-not-produce-an-error-code) + + + ## Error codes If your ODrive is not working as expected, run `odrivetool` and type `hex(.error)` Enter where `` is the axis that isn't working. This will display a [hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) representation of the error code. Each bit represents one error flag. @@ -96,7 +109,7 @@ Check that your encoder is a model that has an index pulse. If your encoder does * Try a different USB cable * Try routing your USB cable so that it is far away from the motor and PSU cables to reduce EMI -## **Firmware Issues** +## Firmware Issues ### Failure to build the firmware when running `make` - Clear out temporary files from previous compiles by first running `make clean` to prevent conflicts. From 6eb53f13e0ac8faceebd035769a9db2d4e02e122 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Wed, 12 Sep 2018 18:59:02 -0700 Subject: [PATCH 37/41] Update control.md --- docs/control.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/control.md b/docs/control.md index 3775c033..f0b2da1d 100644 --- a/docs/control.md +++ b/docs/control.md @@ -6,15 +6,25 @@ The motor controller is a cascaded style position, velocity and current control ### Position loop: The position controller is a P loop with a single proportional gain. -* `vel_cmd = (pos_setpoint - pos_feedback) * pos_gain` +```text +pos_error = pos_setpoint - pos_feedback +vel_cmd = pos_error * pos_gain + vel_feedforward +``` ### Velocity loop: The velocity controller is a PI loop. -* `vel_error_sum += (vel_cmd - vel_feedback) * vel_integrator_gain` -* `current_cmd = (vel_cmd - vel_feedback) * vel_gain + vel_error_sum + vel_feedforward` +```text +vel_error = vel_cmd - vel_feedback +current_integral += vel_error * vel_integrator_gain +current_cmd = vel_error * vel_gain + current_integral + current_feedforward +``` + ### Current loop: The current controller is a PI loop. -* `current_error_sum += (current_cmd - current_fb) * current_integrator_gain` -* `voltage_cmd = (current_cmd - current_fb) * current_gain + current_error_sum + current_feedforward` +```text +current_error = current_cmd - current_fb +voltage_integral += current_error * current_integrator_gain +voltage_cmd = current_error * current_gain + voltage_integral (+ voltage_feedforward when we have motor model) +``` For more detail refer to [controller.cpp](https://github.com/madcowswe/ODrive/blob/master/Firmware/MotorControl/controller.cpp#L86). From 303bb5ae39c24d548890430af2bacf99238bf3fc Mon Sep 17 00:00:00 2001 From: lukkuch Date: Thu, 13 Sep 2018 20:03:09 +0200 Subject: [PATCH 38/41] Update developer-guide.md Corrected Ubuntu toolchain installation guide as described in: https://discourse.odriverobotics.com/t/compile-error-make-fail-on-fresh-ubuntu-16-04-install/962/4 --- docs/developer-guide.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 440f78ac..2a3cfde6 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -52,8 +52,9 @@ $ python --version # should be 3.7 or later #### Linux (Ubuntu) ```bash -sudo apt-get install gcc-arm-none-eabi -sudo apt-get install gdb-arm-none-eabi +sudo add-apt-repository ppa:team-gcc-arm-embedded/ppa +sudo apt-get update +sudo apt-get install gcc-arm-embedded sudo apt-get install openocd sudo add-apt-repository ppa:jonathonf/tup && sudo apt-get update && sudo apt-get install tup ``` From 6d860a05584345f59a98c5e4d11294773af00509 Mon Sep 17 00:00:00 2001 From: tobbelobb Date: Fri, 14 Sep 2018 14:41:38 +0200 Subject: [PATCH 39/41] Update getting-started.md Fixes typo that said `vel_setpoint` instead of `current_setpoint`. --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index fd453bbc..994009f1 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -228,7 +228,7 @@ Let's get motor 0 up and running. The procedure for motor 1 is exactly the same, ### Other control modes The ODrive also supports velocity control and current (torque) control. * **Velocity control**: Set `odrv0.axis0.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL`. You can now control the velocity with `odrv0.axis0.controller.vel_setpoint = 5000`. Units are counts/s. -* **Current control**: Set `odrv0.axis0.controller.config.control_mode = CTRL_MODE_CURRENT_CONTROL`. You can now control the current with `odrv0.axis0.controller.vel_setpoint = 3`. Units are A. **NOTE**: There is no velocity limiting in current control mode. Make sure that you don't overrev the motor, or exceed the max speed for your encoder. +* **Current control**: Set `odrv0.axis0.controller.config.control_mode = CTRL_MODE_CURRENT_CONTROL`. You can now control the current with `odrv0.axis0.controller.current_setpoint = 3`. Units are A. **NOTE**: There is no velocity limiting in current control mode. Make sure that you don't overrev the motor, or exceed the max speed for your encoder. ## What's next? From d24fd25cb74249c318949b6d04c6cd0d707d3b00 Mon Sep 17 00:00:00 2001 From: tobbelobb Date: Fri, 14 Sep 2018 15:04:59 +0200 Subject: [PATCH 40/41] Update commands.md Fixes typo --- docs/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index 64fe1870..7e7fe3da 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -85,7 +85,7 @@ An upcoming feature will enable automatic tuning. Until then, here is a rough tu ### Encoder position and velocity * View encoder position with `.encoder.pos_estimate` [counts] -* View rotational velocity with `.encoder.pll_vel` [counts/s] +* View rotational velocity with `.encoder.vel_estimate` [counts/s] ### Motor current and torque estimation * View the commanded motor current with `.motor.current_control.Iq_setpoint` [A] From f01031f81c7622709960003d03a05afa98db69e1 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Tue, 18 Sep 2018 16:24:11 -0700 Subject: [PATCH 41/41] Update odrivetool.md --- docs/odrivetool.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index 2356266c..6041928d 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -74,7 +74,7 @@ Note that this command will connect to GitHub servers to retrieve the latest fir If you have a non-default configuration saved on the device, ODrive Tool will try to carry over the configuration across the firmware update. If any of the settings are removed or renamed, you will get warning messages.
How to flash a custom firmware
-If you want to flash a specific firmware file instead of automatically downloading one, you can run `odrivetool dfu [path/to/firmware/file.hex]`. +If you want to flash a specific firmware file instead of automatically downloading one, you can run `odrivetool dfu path/to/firmware/file.hex` You can download one of the officially released firmware files from [here](https://github.com/madcowswe/ODrive/releases). You will need one of the __.hex__ files (not the __.elf__ file). Make sure you select the file that matches your board version.