-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
ESP32 PWM #7806
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
The maximum accuracy of LEDC is only 14 bits. MPY currently uses 10 bits, and LEDC is still a little limited for this application. However, as long as the 14 bit accuracy is turned on, it is enough for steering gear control |
ESP32 was improved in 52636fa. I'm not sure how much of the above has been addressed in that commit. |
None of the things mentioned above are in that PR. It is good that it has been fixed for proper use of timers as this is going to be really helpful to me. I didn't study the code enough to notice that there was an issue with the timers. I did find out that the fade functions cannot be stopped once they are started, this is not helpful at all and not having the ability to stop it if it is running is not useful. It will block a call made to change the duty until the fade has finished. I went in and changed Because of the large number of differences between hardware trying to make interfacing that hardware portable between different devices causes limitation to get put into place. So if a device has a lot more function then another in order to keep portability the code that has to be written would end up being for the lesser device. So a person that purchased a device say in this example because they want to have a higher PWM resolution is not going to be able to get the benefits because of a limitation in how the MicroPython code has been written. Perhaps having a builtin attribute in MicroPython that can be used to identify the hardware that MicroPython has been compiled for would be a better solution. This way a user can write code that can easily be moved between devices running MicroPython. An example would be to have the PWM class moved into the specific esp and esp32 modules so a user could write code like this. if BOARD_ESP32:
import esp32 as esp
MAX_DUTY = 65535
elif BOARD_ESP8266:
import esp
MAX_DUTY = 1023
else:
raise ImportError('no ESP board available') This type of design makes more sense to me as it would allow for portability and also allow all features for a device to be available to the user. I have not come across a way to easily identify what hardware MicroPython is running on. It may exist and I have not come across it yet. |
Maybe the ESP32S2 supports 16 bit. I have to go back and look. I may have misread it also. An example is the TWAI interface where the calculation of the bitrate prescalar, time segment 1 and time segment 2 are needed to set the TWAI interface's bitrate. Without knowing if the ESP32 is an S2 or what the revision is the TWAI interface will not function properly. Again I have to go and look again but the same thing may apply to the maximum resolution that can be used for PWM. |
I just checked the docs again for the ESP32 and there is no mention of a 14bit limitation for the ESP32.
now if you go and look at the I just looked at the way to calculate the number of bits that can be used for a given clock and frequency. For a servo the frequency is 50hz and with a clock of 80000000 this is how you get the number of bits'
The above equation returns the value of 20. Which is exactly what the maximum is in the When I said 16 bits is the max, that was not correct either. Having a 20bit resolution for a servo would produce a resolution of 0 - 1048575. that would be increments of 0.000034° (rounded). which no servo that I can afford would be able to do. Maybe you can afford one? LOL. increments of 0.005° that can be attained using a 16 bit resolution I would think are more then sufficient. This is the use case for servos. There could be applications for controlling a light source with 20 bit resolution that may exist. Or controlling the speed of a motor it may also be beneficial. I do not know of anything that would need that kind of resolution |
I did find a couple of macros that can be used that will make it easy to fix.
I believe the second one is the number of bits. so instead of having to do an iteration to figure out the actual bit depth of the duty cycle like seen in the code below unsigned int res = 0;
for (unsigned int i = LEDC_APB_CLK_HZ / newval; i > 1; i >>= 1) {
++res;
}
if (res == 0) {
res = 1;
} else if (res > PWRES) {
// Limit resolution to PWRES to match units of our duty
res = PWRES;
} this can be used instead unsigned int res = (unsigned int) log2(LEDC_APB_CLK_HZ / newval);
if (res > LEDC_DUTY_SCALE_MAX) {
res = LEDC_DUTY_SCALE_MAX;
} There would need to be a method that would return the maximum duty that can be set |
I regret to inform you that the accuracy of the steering gear is not as high as you expect. Usually, there are only 500 or less effective points in the working range. Therefore, it is not necessary to pursue ultra-high PWM resolution. |
I know the accuracy is not that high on a typical RC car style steering gear. But with a 10 bit resolution you cannot even get close to what the accuracy is. using the PCA9685 which has a 12 bit duty that doesn't even provide enough of a range to get to the smallest increment the servo can do. I have not tried anything above 12 bit. 14 bit is about where I thought would be the maximum. PWM is used for a whole lot of other things besides servos. so if the ESP32 is capable of handling a 20bit resolution why not allow the users access to it??? I had suggested in my first post allowing a user to pass a duty resolution bit depth. This way if the resolution for a given frequency produces too much of a range they have the option to decrease that range. |
You can test the firmware code firmware |
If higher accuracy is required. Duty cycle parameters are directly input into floating-point parameters, and the value range is consistent with that of integers |
I need to be down to 0.25° of angle adjustment for the project I am working on. And the code has to be error free and not have the potential to have any glitches in it. |
OK I need to point out a few things with your code and how you are setting the duty cycle In your code you are accepting a float input for the duty and once the C code for the method runs it takes that float and limits it to This is my problem with this. in the end you are stills setting the duty that is in the range of a 14 bit unsigned int. You have the additional overhead hat is needed to do floating point math. You are limiting the 14bit duty cycle to a 10 bit one by setting the maximum allowed to 1023... So you are using 4 bytes of data for the float, then you are using another 4 bytes of data to do the conversion from float to int. You have the duty variable declared as a signed integer which consumes 4 bytes and has a range of -2147483648 to 2147483647. there is no need to waste 2 bytes of memory to have negative numbers as you cannot set the duty to a negative number. and because you are limiting the float to 1023 you are using 4 bytes there when you only need to use 2. There is to much wasted memory the floating point math is causing additional processor load and the end result is you get a 10bit resolution which is exactly how the code is written right now. Plus there is the animal of floating point math errors that can take place. The whole thing can be done keeping the 14bit resolution and using 2 bytes of data instead of 8 and no additional overhead for floating point math. ledc_set_duty(PWMODE, self->channel, (mp_obj_get_int(args[1]) & ((1 << PWRES) - 1)) >> (PWRES - timer_cfg.duty_resolution)) |
In the world of servos if you translated the float so it aligned with 0°-359° That would be beneficial to have. I would then be able to tell it to move the servo to 186.1° and it would move to that angle. In order to know where that angle is a start and stop duty along with a start and top angle would need to be known. so with your code is I pass 500.5 in for the duty the duty would get changed to 8008 to be passed to ledc and 500.55 turns onto 8009 and 500.6 turns into 8010. which seems to work. until I do 500.65 and I get 8010 again and no movement happens. |
The actual input to the duty cycle register is the data rounded after operation, and there will be discontinuity Changing the value range of firmware may lead to errors in historical codes, which is a high risk |
This is incorrect - the IDF-TARGET is set at the board definition level - See my S2 boards for reference. |
OK I see what you are talking about, but one problem. I don't see anything that sets CONFIG_ESP32_REV_MIN. with the TWAI interface this will alter how the program runs depending on if you have a revision 1 or a revision 2+ ESP read the Baudrate Prescaler information from the esp-idf docs |
I just got into really messing about with the code for MicroPython and I am coming from using the firmware that is already compiled. And there is no firmware that is compiled for the ESP32S2-WROVER so I didn't know that this could be specified. |
@kdschlosser You can also specify There is no specific board config for the ESP32S2-WROVER as it's not a board, it's a module that cannot be used on it's own. 2 boards using a ESP32S2-WROVER could be totally different, with different pinouts, number of IIO, and feature set, so board files are specific to a board, not a module. If you feel |
I didn't way anything about that variable and an ESP32S2 being joined at the hip I stated that the MicroPython build program doesn't have any way of easily setting it. I am not sure why you think I am only referring to the S2 as I am not. Those 2 macros define how esp-idf works for the given MCU. If the board revision is set to a 1 (I believe this is the default) but the MCU is a revision 2 MCU everything will function but there could be an expanded feature set that is not being realized unless the revision gets set to a 2. I haven't come across the ability to do that without having to modify the MocroPython build system. With as complex as the esp-idf is and also how complex the MicroPython build system is I don't see people wanting to spend the time to figure it out. I still have to figure out what the actual methods are for PWM because the documentation states to use |
No, you are just not understanding how this all works - you don't get an option at build time to pass stuff because there are no options like that - that is not how the build system works. Nor do you need to learn, modify or change the build system to achieve what you want. It already does what you need. It's super simple. When you build MP esp32 firmware, you do it for a specific board - not for a specific chip or module. In that board definition, you can set and overwrite whatever So you go to the GENERIC_S2 folder (or whatever board you choose to start from), duplicate it, add Please take some time to look over all of the board definitions to understand how they all work. They are not complicated, they just don't work how you think you need to solve this. |
As simple as it is for you there is no documentation on this or that it would even need to be done. Also how is one to know if the thing that is added/changed has been accounted for in MicroPython and that changing/adding it not going to cause an issue?? There is no documentation on what settings can and cannot be changed and what the setting impacts in MicroPython when it is changed. An example is a PR that has been made for the TWAI interface. If the PR gets merged in it's current form then me changing IDK if either of those macros changes the ledc portion of the esp-idf in any way and if they do does it trickle down the changes to the user when running MicroPython? and knowing what exactly has changed would be a nice thing to know. Having to spend many hours sifting through complex code that I am unfamiliar with is not something I would consider to be fun. Even more so if the purpose is to locate what is going on when it could have easily been documented. Now I do understand that trying to keep up with what is happening in the esp-idf would be really hard to do as the code base is pretty large and having to monitor and read every single PR would be insane. But if there are setting/changes that the MicroPython build system can be directed to do via command line or file and the behavior of MicroPython would change because of it documentation of this would be a very large time saver. I actually ordered an Onion Omega2 that runs WRT Linux and can run CPython due to the little quirky things in MicroPython and the ESP32. The PWM duty resolution being one of the things. I found that the extremely low max operating temp of 80C was not going to work in my application. Had the information been readily available for MicroPython I could have saved myself 2 weeks of poking about to see how I could get it to work to the limits of what the ESP32 is capable of doing. |
The state of the documentation and of per port peripheral support are very different conversations to what I was trying to help you with, which was how
Yes they should be documented better - everything in MicroPython could use more fleshed out and better docs....no one is going to argue with you there, but that's simply a Time & Human Resources problem :( Thankfully, MicroPython is open source, so feel free to fork it and work on improving the docs and doing a PR - I'm sure the core team would appreciate the extra help! |
I don't have a problem doing that and I will start with the TWAI portion of the code. Also wouldn't mind building stub files for it either. This way the docs would also be built into it. Gotta figure out how the documentation system works. It doesn't look like typical c style documentation is used at all. |
PWM hardware isn't the only way to generate a servo-friendly signal with an ESP32. I am successfully using RMT channels to drive SG90 servos with very high resolution, reserving PWM channels for motor power. Documentation here.
But beware: in the uPy 1.15 version and perhaps later, when using multiple channels the base frequency changes without relation to the code. I am investigating before opening an issue. With version 1.14 it works fine. |
I will add duty_ns() and duty_u16() |
Could anyone test PWM.duty_u16() and PWM.duty_ns() with an oscilloscope? |
don't have an o-scope. But why would you need one anywho? Loop the output pin into another pin on the ESP32 and check that way. IDK if there is enough "muscle" in the esp32 to be able to do that. You may have to use a second esp32 to get the job done. |
@IhorNehrutsa I have posted some test results here: #7907 which show a severe problem. |
I wonder if my SnapOn VOM has the ability to do this test.. It's actually made by Fluke. |
OK meter didn't support it. I did however write some code for an Arduino to read the PWM. I can confirm the same frequency drift. When I construct the PWM with a value of 50 for the frequency I am seeing 50.02Hz. If I set the frequency to anything from 50Hz to 498Hz the frequency is off by a fraction. The strange thing is the amount it is off is linear up to around 430Hz from 430Hz to 498Hz the amount it is off drops and this drop is also linear. 1000Hz is correct and so is 2000Hz from 2001Hz up to 4999HZ it's off again, 5000Hz is fine. 5001Hz to 9999Hz are wrong, 10000Hz is correct. and above 10k it's not stable at all. I get a bounce at 20k of -3.8% to +4.4% It definitely gets worse at higher frequencies |
So how do you know that your Arduino is right? Taking my oscilloscope, which is specified at a timing accuracy of 10E-6, and a sample rate of 2 GHz: |
100000 100000.5 = 0.0005% deviation Now you see the problem. The exact same issue I am seeing. how far it is off isn't really the point. The point is that a 1Hz change in the frequency causes the error margin to increase by almost 10000%. You are seeing the same issue as I am seeing. |
The question then becomes is this a problem with the ESP it's self or is it a problem in the MicroPython code. I believe it is going to be an issue with the EP32 or the esp-idf SDK. I am going to ask someone at Expressif to run tests on the LEDC portion of the SDK and see if they have the same issue. |
That's caused by the way the frequency is generated - by dividing an input frequency, e,g, 160MHz, with a 8 bit prescaler and an 16 bit integer divider. It cannot create every frequency. The higher the frequency, the worse the result. |
Although, looking at the 100000/100001 example, it should be possible with the given hardware to perform a little bit better, like at 100062 Hz for 100001Hz target, dividing 160Mhz by 1599. |
This is not what I am seeing tho. If I set the frequency to say 498Hz vs 401Hz there is a a much larger error at 401Hz then at 498Hz. The error gets smaller as it gets closer to a frequency that is on a magnitude of 50, 500, 5000 kind of a thing. It's very odd behavior. Being that the clock frequency and the divider bit depth is on the order of 8 you would think that the issue would get worse the further away from a value that is not divisible by 8 evenly. Which doesn't seem to be the case here. |
if a prescalar of 4 is being used against an 80Mhz clock that would put the sampling rate at 20Mhz. Not sure of the mechanics used to generate a correct PWM frequency for 100kHz using a 20Mhz clock. seems to me that the clock is still to high. The clock used is not 160Mhz. it is using LEDC_APB_CLK_HZ which is 80000000. I have not been able to locate what the oscillator tolerance is in any of the specifications so I cannot tell you how much drift can occur. But having a 10000% change from moving the frequency 1Hz is not right at all. |
At 498 I read 498.0107, at 401 Hz I read 401.0047 Hz. That's 21E-6 vs. 11E-6. That seems fine. |
I am 100% sure it is 80Mhz clock. Can you check one other thing with your o-scope. Need to see what the output looks like visually at 100% duty. doesn't mater the frequency. I am detecting a falling and a rising and it shouldn't be there. 100% duty should be constant. |
I am pretty sure I know where the drift is coming from, The LEDC API only has microsecond accuracy. here is an example If wanting a frequency of 700Hz the timing would be 1428.5714285714285714285714285714 microseconds. If only microsecond resolution is available that number would be 1428. 1 / (1428.5 / 1000000) = 700.03500175008750437521876093805 But I am not seeing 700.03Hz I am seeing 700.28Hz which is an even microsecond value. In order to get down to nanosecond resolution A 1Ghz clock would need to be used. 1 / (1428571 / 1000000000) = 700.00021000006300001890000567 So what I think we are seeing is caused by a hardware limitation. While it can be improved by 0.07017502592359547037849429485Hz if rounding was used I believe that is where the issue lies and there is nothing that can be done about it. |
Note 1) Not all frequencies can be generated with absolute accuracy due to ISP-IDF uses 80MHz LEDC_APB_CLK. |
That's my saying all along the line. In addition, the base crystal may not be precise as well. Typical tolerances range for 10 to 30 ppm. And I do not expect that the low cost modules go for the best (and more expensive) crystals/oscillators. |
and you couple the divider not being fractional with only having microsecond timing precision and you don't get a perfect duty cycle along with not having a perfect frequency. I think we are all on the same page with this. @robert-hh, how are you managing to get over 4v of pwm output from the esp32?? |
That's just the ringing of the probe and is not generated by the ESP32. For a quick hookup I use a standard 10MOhm probe with a long tip (3cm) and a long GND clip (12cm). The capacity of the tip and the inductivity of the GND wire form a LC oscillator, which is ping'ed by the fast transition of the ESP32 output. That increases also the displayed rise/fall times. If I use a different tip on the probe (GND, Tip < 1cm) or a different probe, the overshoot much smaller and the rise/fall times decrease. |
OK. that makes sense. I don't know the PWM specification at all. The code I wrote for the Arduino simply measures the top duration and the bottom duration adds them together divides them my 1000000 to get seconds and I divide 1 by that number to get the frequency. the duty I am dividing the off duration by the sum of the on and off and multiplying it by 100 to get the %. The duty is how much on/off time there is in comparison to the total duration of the on and off. so the higher the resolution the smaller the changes in the on and off would be. So the reason why the resolution lowers as the frequency increases is because the on + off decreases as the frequency increases and the ESP32 is unable to measure the time. So as the duty resolution decreases the on/off pulses increase to fill the space allotted by the frequency. This is how I believe it works. I would have to read the specification to be sure, or you can tell me yes or no. The frequency is measured by timing how ling it takes to get from the first arrow to the second.
The duty is the timing from the on period divided by the time measurement above.
The ESP has to decrease the resolution at higher frequencies because the duration of the on + off gets smaller as the frequency increases. So if the duty resolution stayed the same the ESP would have to be able to measure the duration with a much higher precision then it is able to. so to over come that problem it shrinks the resolution there for making the periods larger. |
The ESP like other devices to not measure anything. The simple model of a PWM module consist of a counter with a fixed input frequency F (e.g. 80MHz). The counter has a variable period N and a switch value S, with 0 <= S <=N. The counter starts at N = 0, setting the output value to 1. It it reaches S, it sets the output to 0. If it reaches N, it starts over. So the frequency of the output is F // N (using // for the integer division). The duty rate is S / N. |
it actually has a 20 bit counter for N. The ESP32 determines what the resolution is by both an internal check and also by when gets set by the user. so if you have a 50Hz PWM you can set the duty resolution to any number of bits from 1 to 20. allowing and adjustable amount of duty. it's not "hard coded". I updated the PWM portion of the MicroPython code so it allows a user to adjust the duty resolution when the instance is constructed and also when the frequency is changed. and I added a function that returns the maximum amount of duty that can be set for the given frequency or bit depth which ever one has the lowest duty resolution. https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html#_CPPv416ledc_timer_bit_t |
measure is the wrong term. all it does is count, it is up to the user to know what the crystal frequency is which is what determine the length of time. so at 80Mhz the crystal provides approx 80 million pulses a second. the prescalar tells the ESP to count every X pulse. This brings the 80 million down into a manageable area to be able to produce the on/off times that are needed. |
The LEDC module of esp32 is different from the PWM generated by common timers |
OK, I assumed the for PWM the mcpwm module was used. The LEDC module is different, with the 10+8 bit fractional prescaler and the 20 bit counter. Given that, the result should be more precise than it actually seems. |
@kdschlosser @dpgeorge please close |
…ports Handle HID OUT reports with no report ID
It would be nice to have the full 16 bit resolution available for the duty cycle. In my use case I am controlling a digital servo with it. I didn't want to have to add a PWM IC to the boards I am building because there would be added expense to have the PWM IC soldered onto the boards. The IC's are SMDs with some pretty small pins and doing it by hand would result in a high failure rate if I even managed to get any soldered down properly at all.
One such PWM IC is the PCA9685 and it has a 12 bit duty resolution. This allows me to turn a servo from 0° to 359° using 0.0879° increments. With the EP32 being locked to a 10 bit resolution my increments would become 0.3516° increment which is quite a large difference. if the full 16 bits were available the increments would become 0.0055° if the servo is capable of moving that small an amount. I know the ones that I am using work with 12 bit.
Adding a function or property that would allow a user to obtain what the max duty cycle is would be extremely handy to have.
max_duty = (2 ** timer_cfg.duty_resolution) - 1
It would also be nice to have an additional parameter added to the set frequency and the constructor that would allow a user to set a lower duty resolution. If the parameter is not supplied then the maximum duty resolution would be used. it would be simple to add in a check to ensure that a provided duty resolution was not higher then the maximum if it is then raise an exception.
Now I am not sure if anyone has worked with a high quality digital servo at all. These things are capable of moving 60° in 0.2 seconds and sometimes faster. Without having the
ledc_set_fade_with_step
,ledc_set_fade_with_time
functions exposed in some manner it becomes quite the task to throttle how fast the servo moves. Writing the code to handle that in Python does consume quite a bit of resources where I am sure using that functions would be considerably less.I do not know how the threads work with the ESP32 and if they are "real" threads. If they are it would be helpful to also have the thread safe versions of the functions exposed or have the program automatically use a thread safe version if making a setting change that is not happening from the main thread. I would think there is a way to identify a main thread. I do not know if the non thread safe versions of functions are blocking calls or not. I would imagine the non thread safe fade functions would block until the fade completes.
The ESP32 has a whole bunch of PWM capabilities that are not exposed and it would be handy if they were. From reading the SDK and looking at the ESP32 port files I don't believe it would be hard to add the above mentioned things for someone that has worked with the backend MicroPython code. I would be willing to add the features is someone would explain how the MP_ROM_QSTR function/macro works and where the values that are passed to it is located.
The text was updated successfully, but these errors were encountered: