记录掉进micropython的pyb的Servo的坑
这几天我在学习OpenMV,我就只是想生成一个PWM,然后控制舵机,然后就栽在这个上面了好几天。文档翻来覆去解决不了我的问题。于是记录一下我的解决方案吧。
pyb库的Servo
我是一开始通过https://book.openmv.cc/MCU/pyb.html#servo看到pyb库有一个Servo,看着挺简单的,只需要指定舵机的位置,然后就直接让他转多少角度(angle函数),而且可以设置速度。然后就悲剧了。我仔细阅读了一下angle函数的描述,它的内容如下。
- Servo.angle([angle, time=0])¶
If no arguments are given, this function returns the current angle.
If arguments are given, this function sets the angle of the servo:
angleis the angle to move to in degrees.timeis the number of milliseconds to take to get to the specified angle. If omitted, then the servo moves as quickly as possible to its new position.
我就写了下面这样子的代码。
1 | s1 = Servo(1) # P7 |
似乎可以运行,但是我就考虑到一个问题是,精度问题,我不可能一度一度转,原因是按照距离来算,1米的情况下,转动1度的移动距离是$Distance=\tan(1^\circ) \times 1,\text{m}\approx 0.017455,\text{m}$,几乎都要贴到出界的范围了。只要多一点的就会出去的那种,完全不能忍受。
另外,我发现还有一种是pulse_width,我也不知道为什么这个没效果,反正在我的示波器上,我给了20000作为参数,是没有脉冲的,我就放弃用这个了。
pyb库的Pwm
pyb库的Pwm也是一个坑,在我使用Servo不行了以后就开始看原始Pwm的代码。我知道这玩意不难,但是这文档也太简陋了,不管是星瞳科技的文档还是micropython的文档都一塌糊涂。根本解决不了我的问题。
星瞳科技的文档就下面这段,我一开始看着,还行啊,通过控制占空比来控制。
1 | from pyb import Pin, Timer |
因为之前Servo的经验,然后我就尝试pulse_width_percent这个函数能不能传入一个小数,这样子不就提高精度了吗?要不然,一个180度的舵机,加百分之一,一下就多了1.8度,这直接超过边界了,完全不可控。
我就把这个第六行的代码改成ch.pulse_width_percent(50.1),然后,这OpenMV的IDE,就给我弹窗,爆一个很可爱的错误。大大的TypeError: can't convert float to int。然后我就开始读这个函数的文档。文档关于这个函数的描述如下
- timerchannel.pulse_width_percent([value])¶
Get or set the pulse width percentage associated with a channel. The value is a number between 0 and 100 and sets the percentage of the timer period for which the pulse is active. The value can be an integer or floating-point number for more accuracy. For example, a value of 25 gives a duty cycle of 25%.
他说是0到100的值,然后说是25就代表25%的占空比,那我咋不能用50.1表示50.1%的占空比呢。它也没有说明类型,必须等到填进去一个浮点数,才给你报错。
然后我就没办法,我就只能找一个设置原始值的函数,还真让我找到了,是pulse_width。这个参数也是有意思,他也不说传入的value是什么东西。
timerchannel.pulse_width([value])¶
Get or set the pulse width value associated with a channel. capture, compare, and pulse_width are all aliases for the same function. pulse_width is the logical name to use when the channel is in PWM mode.In edge aligned mode, a pulse_width of
period + 1corresponds to a duty cycle of 100% In center aligned mode, a pulse width ofperiodcorresponds to a duty cycle of 100%timerchannel.callback(fun)¶
Set the function to be called when the timer channel triggers.
funis passed 1 argument, the timer object. IffunisNonethen the callback will be disabled.timerchannel.capture([value])¶
Get or set the capture value associated with a channel. capture, compare, and pulse_width are all aliases for the same function. capture is the logical name to use when the channel is in input capture mode.
timerchannel.compare([value])¶
Get or set the compare value associated with a channel. capture, compare, and pulse_width are all aliases for the same function. compare is the logical name to use when the channel is in output compare mode.
这文档也是逆天,叽里咕噜说了一大堆,跟没说没啥区别。也不告诉我,这个参数啥意思。
然后呢,我就一个一个值试,然后拿示波器看波形。一直试到10000差不多有点波形了。我就两倍两倍加,发现40000差不多是慢占空比,然后我问了一下GPT,50hz下,40000的pulse_width是满占空比,然后它就一顿推导,发现单位是ns,因为时钟是2Mhz的,这参数的value也不标一下单位,如果是秒或者毫秒,我不得加小数传入,单位又不一样了。
探索pyb源码
然后我就开始翻micropython的pyb库的源码,终于在https://github.com/micropython/micropython/blob/8f8f8539827a4d38dd51e4960fe54a0ed8ab08ea/ports/stm32/timer.c#L1581,发现了一些东西。确实是得用channel的pulse_width 来设置最原始的脉冲宽度值。它没有说明单位是什么,就纯粹是计数值。如果是计数值,那文档里吱个声也就不用我一个一个猜了。
然后仔细看Servo的实现的源码,那真是一言难尽了。一看对象定义,它这里是有定义的,pulse是以10us为单位,然后到文档里面就没有了。真是坑死我也。有点感觉是意义不明确的10us。
1 | typedef struct _pyb_servo_obj_t { |
然后我就去看我之前出错的angle函数。按照我之前出错的情况来看,是因为没有启用MICROPY_PY_BUILTINS_FLOAT这个宏,使用他以mp_obj_get_int方式打开我的浮点数,所以就炸了。但是如果是用mp_obj_get_float拿的我传入的浮点数,应该是没问题的。
1 | /// \method angle([angle, time=0]) |
最后,我发现一个惊喜的地方就是pyb库的代码风格确实很好。看下面这个callback。他基本上所有的外设都会有一个xxx_irq_callback,然后再在中断服务函数里面调用。这种方案可以统一处理中断函数的编写,而不是让外设的中断代码散落到各个地方。
1 | void servo_timer_irq_callback(void) { |
最终解决方案
反正我是没有耐心去调试pyb库的Servo了,我是觉得我用不好它的Servo实现,我就基于pyb的Pwm实现了一个MServo类。对于精度精准控制的情况下,舵机的Pwm必须是以最原始的改变脉冲宽度来控制的。所以我传入的就是计数,这样子会容易控制一些。我也不去搞他那个10us的奇怪东西了,我感觉那个是最奇怪的地方,然后文档也不解释,代码就稀里糊涂写。
1 | from pyb import Pin, Timer |
写于2025年7月12日,23点30分。
