Skip to content

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

Closed
kdschlosser opened this issue Sep 16, 2021 · 54 comments
Closed

ESP32 PWM #7806

kdschlosser opened this issue Sep 16, 2021 · 54 comments

Comments

@kdschlosser
Copy link

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.

@wangshujun-tj
Copy link

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
Mcpwm is a module more suitable for realizing general PWM, and the disclosure of this information is fairly good
In the forum, I presented an example of using RMT module to realize steering gear drive, which can realize high-precision steering gear control without modifying firmware, and the setting is a little troublesome

@dpgeorge
Copy link
Member

ESP32 was improved in 52636fa. I'm not sure how much of the above has been addressed in that commit.

@kdschlosser
Copy link
Author

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 #define PWRES (LEDC_TIMER_10_BIT) to #define PWRES (LEDC_TIMER_16_BIT) So the full duty resolution of 0-65535 would be available for a 50hz frequency instead of 0-1023 which is what a 10 bit resolution would give vs a 16 bit one. The ESP32 can handle 16 bit resolutions where the ESP8266 is limited to 10 bit I believe.

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.

@kdschlosser
Copy link
Author

Maybe the ESP32S2 supports 16 bit. I have to go back and look. I may have misread it also.
I am using the ESP32S2. Custom firmware needs to be compiled when using the ESP32S2. The build process for MicroPython has no consideration for setting the idf-target command line switch when it calls idf.py. There is also setting the revision using the macro CONFIG_ESP32_REV_MIN that has not been considered either. using the idf-target switch is what sets the CONFIG_IDF_TARGET_ESP32S2 macro to a 1.

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.

@kdschlosser
Copy link
Author

I just checked the docs again for the ESP32 and there is no mention of a 14bit limitation for the ESP32.

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html?highlight=ledc#_CPPv416ledc_timer_bit_t

The duty resolution is normally set using ledc_timer_bit_t. This enumeration covers the range from 10 to 15 bits. If a smaller duty resolution is required (from 10 down to 1), enter the equivalent numeric values directly.

now if you go and look at the ledc_timer_bit_t enumeration you will see that the minimum value in the enumeration is LEDC_TIMER_1_BIT and the maximum value is LEDC_TIMER_20_BIT leading one to think he maximum PWM duty resolution that can be supported would actually be 20 bits. again this all depends on the clock that is used and MicroPython uses the ADC clock which is 80000000.

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'

uint8_t num_bits = (uint8_t) log2(80000000.0 / 50.0)

The above equation returns the value of 20. Which is exactly what the maximum is in the ledc_timer_bit_t enumeration

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

@kdschlosser
Copy link
Author

I did find a couple of macros that can be used that will make it easy to fix.

LEDC_DUTY_CYCLE_MAX and LEDC_DUTY_SCALE_MAX

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

@wangshujun-tj
Copy link

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 have made an attempt to improve the resolution of low-frequency PWM. The 14 bit accuracy is used, but it is fully compatible with the original API. The method is to allow the duty cycle to transfer floating-point numbers, which can achieve 16 times the resolution improvement in the period below 200 Hz. The disadvantage is that the read duty cycle becomes a floating point number, and errors will be reported on specific occasions. If you are interested, you can test it first

@kdschlosser
Copy link
Author

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.

@wangshujun-tj
Copy link

You can test the firmware

code
machine_pwm.zip

firmware
mpy-117.zip

@wangshujun-tj
Copy link

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

@kdschlosser
Copy link
Author

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.

@kdschlosser
Copy link
Author

kdschlosser commented Sep 22, 2021

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 1023.9999 and the it multiplies that by 16 and adds 0.5 to it producing 16384.4984 and because you are dropping it into a variable that has been declared an int it gets turned into 16384..

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))

@kdschlosser
Copy link
Author

kdschlosser commented Sep 22, 2021

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.

@wangshujun-tj
Copy link

The actual input to the duty cycle register is the data rounded after operation, and there will be discontinuity
The 16384 problem you worried about earlier will be eliminated in the later and operation, but it will lead to output inversion. This is really a bug. The original firmware processing method is to use AND operation to realize cycle processing, so it may be more reasonable to remove the upper and lower limits
The servo has 0.5ms-2.5ms version and 1ms-2ms version, and there are many versions corresponding to the angle in this range. It is recommended to package it again in py code for angle conversion and acceleration and deceleration operations. In the worst case, the 1ms-2ms version will also have more than 1600 output values, which exceeds the effective resolution of servo. I actually test the servo of mg996, In fact, 4us is the minimum effective resolution. 0.5ms-2.5ms corresponds to 500 points. In fact, both ends are slightly expanded

Changing the value range of firmware may lead to errors in historical codes, which is a high risk

@UnexpectedMaker
Copy link
Contributor

Maybe the ESP32S2 supports 16 bit. I have to go back and look. I may have misread it also.
I am using the ESP32S2. Custom firmware needs to be compiled when using the ESP32S2. The build process for MicroPython has no consideration for setting the idf-target command line switch when it calls idf.py. There is also setting the revision using the macro CONFIG_ESP32_REV_MIN that has not been considered either. using the idf-target switch is what sets the CONFIG_IDF_TARGET_ESP32S2 macro to a 1.

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.

This is incorrect - the IDF-TARGET is set at the board definition level - See my S2 boards for reference.
ESP32-S2 boards build fine in MicroPython.

@kdschlosser
Copy link
Author

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

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/twai.html?highlight=twai#bit-timing

@kdschlosser
Copy link
Author

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.

@UnexpectedMaker
Copy link
Contributor

@kdschlosser You can also specify CONFIG_ESP32_REV_MIN in a specific boards sdkonfig.board file.

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 CONFIG_ESP32_REV_MIN should be set for all S2 boards, then please open a new issue specific to this, instead of mixing the discussion about it inside this PWM issue as it's just going to get lost here.

@kdschlosser
Copy link
Author

kdschlosser commented Sep 24, 2021

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 freq and duty in the esp32 quick reference but when I look for more detailed information under the Port specific libraries for the ESP32 there is no information for machine.PWM. So then I look under MicroPython specific libraries and there is machine.PWM but it has completely different method names. This leaves me having to thumb through the code to try and figure out what things are and what values need to be passed. Documentation inconsistencies has caused me to spend several hours looking for how the MCU revision gets set thinking that it's probably not documented only to find out that it's not implemented. So now the question is why has it not been implemented? will it cause an issue if it does get set. and have the bits in the esp-idf that use this been considered when MicroPython uses them?

@UnexpectedMaker
Copy link
Contributor

UnexpectedMaker commented Sep 24, 2021

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.

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. CONFIG_ESP32_REV_MIN is just an sdkonfig setting.

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 sdkonfig settings you like. You do it on a specific board and then you build that firmware for that board. Then you have it how you want it.

So you go to the GENERIC_S2 folder (or whatever board you choose to start from), duplicate it, add CONFIG_ESP32_REV_MIN 2 to the sdkonfig.board file and then build it. Done.

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.

@kdschlosser
Copy link
Author

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 CONFIG_ESP32_REV_MIN 2 so it has a value of 2 would not change the constructor so it accepts 12500, 16000 and 20000 as would be expected. Even tho I have specified the correct revision for the MCU the bitrate macros that are defined in the isp-idf for the 3 mentioned bitrates are not added to the constructor. The same thing applies for the 'CONFIG_IDF_TARGET_ESP32S2' and dealing with the TWAI interface.

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.

@UnexpectedMaker
Copy link
Contributor

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 set-target and adding skconfig options work for the ESP32 port.

Even more so if the purpose is to locate what is going on when it could have easily been documented.

...the behavior of MicroPython would change because of it documentation of this would be a very large time saver.

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!

@kdschlosser
Copy link
Author

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.

@lcse66
Copy link

lcse66 commented Oct 4, 2021

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.

from machine import Pin
import esp32

class plane:
	def __init__ (self, epin, apin, lpin, rpin, spin):
		self.eRMT = esp32.RMT(0, pin=Pin(epin), clock_div=80)
		self.eRMT.loop(True)
		self.aRMT = esp32.RMT(1, pin=Pin(apin), clock_div=80)
		self.aRMT.loop(True)

[...]

	def ail (self, val): # val is float value in range +-50
		z = 1640 # tune for servo intermediate position
		g = 16 # gain, max 1000/50 (SG90 servo)
		if val > 50: val = 50
		if val < -50: val = -50
		t = int(z - g * val)
		self.aRMT.write_pulses((t, 5000 - t), start=1)


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.

@IhorNehrutsa
Copy link
Contributor

I will add duty_ns() and duty_u16()

@IhorNehrutsa
Copy link
Contributor

Could anyone test PWM.duty_u16() and PWM.duty_ns() with an oscilloscope?

@kdschlosser
Copy link
Author

kdschlosser commented Oct 16, 2021

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
Copy link
Contributor

I tested frequency and duty cycle with ZT102
image
I see a little deviation in the duty cycle.

ESP32 can generate PWM frequency from 1Hz to 40MHz.
40MHz is not for self-tests.

@robert-hh
Copy link
Contributor

@IhorNehrutsa I have posted some test results here: #7907 which show a severe problem.
I have a ZT303 and connected it as well, since you mentioned it. At lower frequencies (<= 1000) the results seem fine. At 10k, the error is still ~ 1%. At higher frequencies there is an substantial error. But the frequency range of that meter is impressive.

@kdschlosser
Copy link
Author

I wonder if my SnapOn VOM has the ability to do this test.. It's actually made by Fluke.

@kdschlosser
Copy link
Author

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

@robert-hh
Copy link
Contributor

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:
PWM OSC-Reading (kHz)
1000 1.000005
2000 2.00001
5000 5.000025
10000 10.00005
20000 20.0001
50000 50.00025
100000 100.0005
500000 500.0024
1000000 1.000004 MHz
10000000 10.00005 MHz
So the PWM numbers are exceptionally accurate for the "even" frequencies. On other frequencies less. Small steps and "odd" frequencies are less accurate:
321456 321.933 kHz
100001 100.251 kHz
1001 1.001006 kHz
10001 10.005 kHz

@kdschlosser
Copy link
Author

100000 100000.5 = 0.0005% deviation
100001 100251 = 0.249% 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.

@kdschlosser
Copy link
Author

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.

@robert-hh
Copy link
Contributor

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.

@robert-hh
Copy link
Contributor

robert-hh commented Oct 16, 2021

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.
So it looks like the prescaler is set to 4.

@kdschlosser
Copy link
Author

The higher the frequency, the worse the result.

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.

@kdschlosser
Copy link
Author

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.

@robert-hh
Copy link
Contributor

robert-hh commented Oct 16, 2021

At 498 I read 498.0107, at 401 Hz I read 401.0047 Hz. That's 21E-6 vs. 11E-6. That seems fine.
If the clock is 80Mhz, you should expect that beyond ~1.2KHz the prescaler is 1. Maybe the smallest prescaler that can be set is 2. And then you can make yourself a table, which frequencies you can achieve by dividing 80Mhz/prescaler by integers.

@kdschlosser
Copy link
Author

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.

@kdschlosser
Copy link
Author

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 / 1000000) = 700.28011204481792717086834733894 or 700.28Hz which is exactly what I am seeing. With the Arduino I am able to calculate the time to 1e-7 so fractional microseconds.

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.
1 / (1429 / 1000000) = 699.79006298110566829951014695591 = 0.20993701889433170048985304409 (drift)
1 / (1428 / 1000000) = 700.28011204481792717086834733894 = 0.28011204481792717086834733894 (drift)
1 / (1427 / 1000000) = 700.77084793272599859845830413455 = 0.77084793272599859845830413455 (drift)

While it can be improved by 0.07017502592359547037849429485Hz if rounding was used
1.0F / ((float) ((uint16_t) (1428.5714285714285714285714285714F + 0.5F)) / 1000000.0F) = 699.79006298110566829951014695591Hz

I believe that is where the issue lies and there is nothing that can be done about it.

@robert-hh
Copy link
Contributor

Need to see what the output looks like visually at 100% duty.

This is NOT constant high. When setting duty() to 1023, I still get a small low pulse with a width varying with the set frequency:

Freq    Pulse width
10M     12.5ns
1M      12.5ns 
100k    25ns
10k     100ns
1k      975ns

P.S.: The granularity of 12.5ns at 10MHz means, that the highest input frequency for the PWM module is indeed 80MHz. At 10MHz, I get a duty cycle as multiples of 12.5 ns. See the attached pic, where I overlayed all duty cycles from 0 to 1023
pwm_granularity
.

@IhorNehrutsa
Copy link
Contributor

Note 1) Not all frequencies can be generated with absolute accuracy due to
the discrete nature of the computing hardware. Typically the PWM frequency
is obtained by dividing some integer base frequency by an integer divider.
For example, if the base frequency is 80MHz and the required PWM frequency is
300kHz the divider must be a non-integer number 80000000 / 300000 = 266.67!
In fact, after truncating, the divider is set to 266, and the PWM frequency
will be 80000000 / 266 = 300751.9 Hz, but not 300kHz.
If the divider is set to 267(rounding), then the PWM frequency
will be 80000000 / 267 = 299625.5 Hz, but again not 300kHz.

ISP-IDF uses 80MHz LEDC_APB_CLK.
ISP-IDF does'n round the divider!!!

@robert-hh
Copy link
Contributor

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.

@kdschlosser
Copy link
Author

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??

@robert-hh
Copy link
Contributor

robert-hh commented Oct 17, 2021

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.
But the standard probe is easy to handle, and if you know the errors, it's fine.

@kdschlosser
Copy link
Author

kdschlosser commented Oct 17, 2021

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.

@robert-hh
Copy link
Contributor

robert-hh commented Oct 17, 2021

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.
The ESP has a 16 bit counter for N, giving 65535 as largest value. With 80Mhz, the lowest frequency is ~1220.5 Hz, with a granularity of 65536 for the duty rate. If you want to have a frequency of 100kHz, you have to set N to 800, resulting in a granularity for the duty rate of 800. With 801 as N, you get ~99.875 kHz, with 799, you get 100.125kHz. For 10MHz, N has to be 8, and you end up with 8 possible different duty rate settings.
That's the basic operation. The ESP32 has a few extensions to that scheme. It can change F to F' = F // P (P = prescaler, range 1..256) as well so it can go to lower frequencies than 1220.5 Hz. The counter can count up and down, extending the range further by two, and more. I did not look into the sources how the ESP firmware determines N and P, which define the frequency precision. A simple attempt would just make N as large as possible for the best duty rate granularity. If the objective is best frequency match, it could try to find a value pair for N * P, such that F // ( N*P) matches best the target frequency.

@kdschlosser
Copy link
Author

The ESP has a 16 bit counter for 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
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html#_CPPv4N19ledc_timer_config_t15duty_resolutionE

@kdschlosser
Copy link
Author

The ESP like other devices to not measure anything.

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.

@wangshujun-tj
Copy link

wangshujun-tj commented Oct 18, 2021

The LEDC module of esp32 is different from the PWM generated by common timers
Its cycle value can only use the integer power of 2, rather than using any end point (ARR) like common timers. The PWM cycle of esp32 is obtained based on an 10+8 bit prescaler, so only a small number of frequencies are accurate. Moreover, the frequency divider still uses decimal frequency division, which will show periodic jitter
In esp32, the hardware more suitable for generating PWM is the mcpwm module. The behavior of this module is very close to the common timer implementation. However, the absence of this hardware in C3 and S2 versions will lead to too large differences between versions.
Another suitable module is RMT, but there are too few channels available in the C3 version

@robert-hh
Copy link
Contributor

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.

@IhorNehrutsa
Copy link
Contributor

See the latest commits of #7907
Please report bugs to #7907

@IhorNehrutsa
Copy link
Contributor

@kdschlosser @dpgeorge please close

tannewt pushed a commit to tannewt/circuitpython that referenced this issue Apr 7, 2023
…ports

Handle HID OUT reports with no report ID
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants