Skip to content

Changed DAC.write() to 12 bit resolution. DAC.write() is now aligned wi... #1130

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
wants to merge 1 commit into from

Conversation

ul5255
Copy link

@ul5255 ul5255 commented Feb 22, 2015

...th ADC.read(). The drawback here is that existing code which uses the DAC.write() method must be changed. I am open to suggestions how to do it differently. E.g. introduce a keyword parameter like AC.write(... , resolution_bits= [6, 8, 10, 12]) with 8 being the default.

@ul5255
Copy link
Author

ul5255 commented Feb 22, 2015

@ul5255
Copy link
Author

ul5255 commented Apr 27, 2015

May I kindly ask what I can do to get this pull request merged? If any projct collaborators have concerns I'd also appreciate feedback to improve the pull request. The request is a little over two months old now.

@dpgeorge
Copy link
Member

@ul5255 yes, sorry, I left both these ADC/DAC PRs because I never got a big enough chunk of time to sit down and think about them. The problem with this one (even though it completely follows the guidelines, thanks!) is that it'll break existing code. Yes we will need to eventually make a backwards incompatible change to support the ADC and DAC properly, but I want to make sure we do it properly and don't have to make another change down the track.

To be really generic, DAC.write should take a number between 0.0 and 1.0 and scale it to the current bit-resolution. But we don't want to do that because forcing floating-point number usage is not good on a microcontroller. So we could make it a percentage (0 to 100) but then you don't get full resolution.

So maybe we want DAC.write which takes 0.0 to 1.0 and scales it, and DAC.writeraw which takes an integer between 0 and maximum value. And then we'd need to have similar ADC.read and ADC.readraw.

Any other ideas?

@chrismas9
Copy link
Contributor

A really generic interface needs to support bipolar/unipolar or signed/unsigned sensors/transducers and/or signed/unsigned converters. For example it is common to connect an accelerometer (bipolar with 0g biased to Vref/2) to a unipolar ADC. Same with AC hall effect current sensors, audio, etc. It is also common to connect unipolar sensors (eg a load cell measuring 0 to 1000kg) to a precision 24bit ADC with differential inputs and signed data out. You add a DC offset in the input buffer so 0 to sensor full scale gives +/- ADC full scale at the differential inputs, eg 0-10V from sensor gives +/-1V at the ADC inputs.

Instead of raw I would prefer data left justified into a word or double. This makes the integer data much more hardware independent. Full scale is always 64k or +/- 32k. You can stream audio from the 12bit STM32 ADC directly to its 10bit DAC without having to know the resolution of the ADC and DAC and scaling the data. The same streaming code just works on any hardware up to 16 bit ADC/DAC. For 24 and 32 bit converters we need 32bit raw interface (an yes TI and ESS make 32bit ADC's).

So I suggest 6 read and write methods:

ADC.readfloatsigned -1.0 to 1.0
ADC.readfloatunsigned 0 to 1.0
ADC.readwordsigned -32k to 32k
ADC.readwordunsigned 0 to 64k
ADC.readdoublesigned -2^31 to 2^31
ADC.readdoubleunsigned 0 to 2^32

It is important for signed and unsigned reads to work with both unipolar and bipolar ADC's to handle the common mismatch of sensor and ADC, (eg microphone and STM32 ADC). Same principals apply to DAC.write as the same mix of transducers and DACs applies (eg speaker on STM32 DAC).

I will be using Micro Python with both internal and high res external ADC and DAC, so these are all real world scenarios.

@pfalcon
Copy link
Contributor

pfalcon commented Apr 27, 2015

(even though it completely follows the guidelines, thanks!)

Well, almost. First line should be "title" line, length <~75 chars, detailed description should be after a blank lines, all still formatted in ~75 char lines. (All that is common git wisdom.)

@pfalcon
Copy link
Contributor

pfalcon commented Apr 27, 2015

Any other ideas?

My idea on this follows core of what @chrismas9 says. But let me illustrate it differently. On AVR (or maybe it was MSP430, don't remember), ADC value reg could have significant bits either left or right adjusted within word. Align right, and you know you have upper bound on values. Adjust left, and you're just using full word range, and that's easily portable to other ADC sizes.

We should "adjust left" with a bit length which makes sense. I never saw general-purpose MCUs with more than 14 significant bits. So, 16 bits for ADC/DAC sounds good imho. Actually, 16 bits is already over 16-bit arch Python int type range, so maybe can be extended to 24 bits. But 2^24 isn't the most popular power, maybe 2^20 (=~ 10^6) is good. Something like that.

@pfalcon
Copy link
Contributor

pfalcon commented Apr 27, 2015

So I suggest 6 read and write methods:

Sorry, but that's bloat.

I will be using Micro Python with both internal and high res external ADC and DAC, so these are all real world scenarios.

So, you have few choices:

  1. Implement it and submit your patches for review. If they pass review, they will be merged. Note that if you submit your patches too late, when we fixed the API, they may be rejected because of that (so, "release early, release often!").
  2. Maintain those patches/modules on your own. Then you can maintain them either as opensource, or just for yourself.

We'll see if you choose open-source way, like we did. And we'll see what you will reply to people coming and telling that they what that, bunch of bits more, and cream on top. We're telling that we can't satisfy everyone and shoot to cover 90% of usecases, not 100%. Special needs usually require special solutions.

@chrismas9
Copy link
Contributor

We should "adjust right" with a bit length which makes sense

Do you mean "adjust left"?

I never saw general-purpose MCUs with more than 14 significant bits.

Most of the Freescale Kinetis MCU's have 16bit SAR ADC including sub $4 Cortex M0+ (KL27). Also STM32F373. These are not noise free to 16 bit so 15bits would be ok and would save memory over 20 or 24. However Freescale, TI and others now have general purpose MCU's with 24bit delta sigma ADC's as well

@chrismas9
Copy link
Contributor

But 2^24 isn't the most popular power

24bit converters are the most common high resolution delta sigma and audio converters. There are a lot more Cortex MCUs with 24bit ADC than 20bit. If you go beyond 15bit 24 would better than 20. There is not much beyond 24bit and it probably won't see mainstream use so I would be happy with a 24 bit limit.

@chrismas9
Copy link
Contributor

We'll see if you choose open-source way

I am not proficient in C so I cannot contribute to upy code. However I believe in open source and am contributing on the hardware side and in any way I can.

My suggestions about signed converter data is not a selfish request for someone else to write drivers for me but a suggestion to make upy better for anyone without extensive hardware knowledge, eg first time users of Pyboard and AMP. Many users will not understand that the microphone is biased to 1.65V or that if they multiply the ADC data by 2 to increase the volume they will get horrible half wave rectified noise out of the DAC. With signed audio data that multiply would just work.

@HenrikSolver
Copy link
Contributor

Any attempt to abstract anything that is connected to the ADC pin of the MCU will be bloat to most people. It will be difficult enough to find a least common denominator between the ADCs in STM32 and PIC that handles more than the bare minimum. Code written to use the ADC is in its nature application and hardware dependant since it often relies on signal conditioning hardware on the outside of the chip. So I think it is better to have a common basic feature set with extensions that are chip specific.

@pfalcon
Copy link
Contributor

pfalcon commented Apr 27, 2015

Do you mean "adjust left"?

Yep, fixed.

@dpgeorge
Copy link
Member

So I think it is better to have a common basic feature set with extensions that are chip specific.

Yes I think we should try to cover 80-90% of use cases with a good, simple API, but also provide a clear path for extensions. Eg we say that ADC.read() returns a 24-bit (or whatever) left-adjusted unsigned value, and any extensions (if supported) are provided by keywords such as ADC.read(bit_width=32, signed=True).

@HenrikSolver
Copy link
Contributor

An alternative to always return 24 bits is to return the "native" number of bits. And addition provide an API for the python program to get information about the ADC, like the number of bits or max-min value.

@ul5255
Copy link
Author

ul5255 commented Apr 28, 2015

I'm fine-tuning my first proposal. This is under the assumption that MicroPython should run on any MCU with on-chip DACs up to 16bit. 24bit DACs (Are there any MCU on-chip ones which really deliver even close to this performance???) are left out: This should be handled probably by a DACHighRes class.
Changes to DAC class:

  • introduce a keyword parameter like so: dac = DAC(pin, resolution_bits= [6, 8, 10, 12]) with 8 being the default for backwards compatible reasons
  • use right aligned values for backwards compatibility: I do not see advantages of using left alignment but feel free to convince me
  • introduce a property resolution_bits to the DAC class, which returns the resolution set during DAC.__init__() like for example max_value = 2**dac.resolution_bits - 1
  • DAC.write() and DAC.write_timed() will support (arrays of) uint16_t values, this should be transparent to existing code

I think this proposal is fully backwards compatible and will allow support for up to 16bit DACs.

In addition I would suggest to introduce another class called SignalGenerator which is more flexible than the DAC.write_timed() offered today. I've had some initial work going but will continue there after this issue is settled.

@chrismas9
Copy link
Contributor

I do not see advantages of using left alignment but feel free to convince me

Left justified data allows for much simpler portable code between platforms with different resolutions. With right justified data STM32 (12bit ADC, 12bit DAC) scripts won't run on say Teensy Kinetis (16bit ADC, 12bitDAC) without the complication of determining the resolution and shifting the data to a common precision. Most of the pyb library functions and classes are compatible on all MCU's, eg UART,I2C, SPI, GPIO, PWM and allow portable scripts to run unchanged on different MCU's. Using left justified analog data extends this compatibility to ADC and DAC. It also makes ADC and DAC data compatible on the same MCU with different ADC and DAC resolutions including pyb which only supports DAC in 8 bit mode. Micro Python is already ported to STM32, Kinetis, PIC, CC3200 and ESP8266 and there will be more, so code portability is useful.

Considering the comments above I now think 15bit would be a better default precision. This will cover the 80 - 90% requirement including Kinetis (less than 15 bit noise free), save memory and yield numbers that are not unwieldy. For example signed values are important for audio, accelerometers, etc and15 bit signed/unsigned conversion is just add or subtract 16384. 16384 is an easy number to remember and never changes for left justified 15bit data. The magic number for 24bit left justified data would be 8388608 which I a lot harder to remember. For high res converters ADC.read should return the most significant 15bits and ADC.read(bit_width=24) would return the full value. Same applies to DAC.write.

We should "adjust left" with a bit length which makes sense (@pfalcon)
Eg we say that ADC.read() returns a 24-bit (or whatever) left-adjusted unsigned value (@dpgeorge)

Any other convincing or contra suggestions?

@pfalcon
Copy link
Contributor

pfalcon commented Apr 29, 2015

I do not see advantages of using left alignment but feel free to convince me

That's because you keep thinking that there should be resolution_bits parameter. There should not be. And then everything falls into place - you should just use full dynamic range (from min to max value) and don't care about "resolution".

The above doesn't answer how to deal with block transfers (e.g. via DMA), but the interface of singular "analog" pins should be just like the above - without forcing people to deal with hardware-dependent details, uninteresting for quite a lot of tasks.

@blmorris
Copy link
Contributor

@pfalcon -

That's because you keep thinking that there should be resolution_bits parameter. There should not be. And then everything falls into place - you should just use full dynamic range (from min to max value) and don't care about "resolution".

This is an excellent point, and I would like to take it a little bit further. If you treat ADC / DAC values as falling between 0 and 1 (or between -1 and +1) then left-justified readings effectively provide fixed-point (versus floating point) data. Once you have that, your resolution determines the precision of your readings, but doesn't change the maximum value that you need to consider.

The downside is that while fixed-point numbers are used routinely in signal processing applications, users of high-level languages like Python will be mostly unfamiliar with them. If you have 32-bit floats available then you get a direct correspondence with fixed point data up to ~20-bit resolution, but you can't count on floats being available on all MCU's, and without floats or fixed-point data types you are just dealing with integers that have been left-shifted, and you don't necessarily know how much of a left shift has been applied.

@pfalcon
Copy link
Contributor

pfalcon commented Apr 29, 2015

but you can't count on floats being available on all MCU's

Exactly, that's the issue. Introducing fixed-point numbers may be a possible future option, but we should not count on it either.

and you don't necessarily know how much of a left shift has been applied.

Again, you don't need to know that, just to be able to read analog sensor or control intensity of a LED. We just need to standardize on a range which values may take, that's all people should know. Good reasons for 2^16, 2^20, 2^24 were given above, now just need to decide and standardize.

@chrismas9
Copy link
Contributor

Some MCU's have variable resolution vs sample rate choice. Eg MSP432 (new low power M4F from TI) has 8, 10, 12 or 14 bit resolution at sample rates from 1.8 MS/s to 1 MS/s. ADC.read_timed can automatically or from parameter adjust the resolution down at high sample rates. It would be confusing and complicated if the temperature reading changed from 100 degrees to 25 degrees just because you bumped up the data rate. Left justified data prevents this.

The above doesn't answer how to deal with block transfers (e.g. via DMA), 

Some ADC's can left align their data, others can't and DMA controllers can't shift on the fly. So for DMA maybe we need something like ADC.read_buf that transfers a block of raw ADC data from the DMA buffer to a correctly aligned user buffer. Likewise DAC.write_buf to fill the DMA buffer with raw formatted (8, 12bit or whatever to suit the DAC).

@chrismas9
Copy link
Contributor

extensions (if supported) are provided by keywords such as ADC.read(bit_width=32

Would it be better for portability to define the alignment range when creating the analog object instead of in every read or write. That way you only have to edit one line to reuse say 16bit code on a new target with 24 bit ADC.

adc = pyb.ADC(pin, bit_width=24) # create an analog object from a pin
val = adc.read() # read an analog value

@chrismas9
Copy link
Contributor

Instead of shifting left you could oversample. If you add 8 consecutive ADC readings together (4 microseconds on STM32) you get a 15 bit result and 1.5bit or 9db improvement in signal to noise ratio.

@ul5255
Copy link
Author

ul5255 commented Apr 30, 2015

I'd like to remind everyone that this is a pull request to unlock the full 12bit resolution of the DACs of the MCU on the PyBoard. Discussions related to ADC resolution, oversampling etc. should go someplace else.

@ul5255
Copy link
Author

ul5255 commented Apr 30, 2015

That's because you keep thinking that there should be resolution_bits parameter. There should not be. And then everything falls into place - you should just use full dynamic range (from min to max value) and don't care about "resolution".

  1. left-adjusted values give an implicit MAX_VALUE that is determined by the type of the parameter of DAC.write() and thus not easliy discoverable by code, the MIN_VALUE is determined by the resolution
  2. right-adjusted values give the explicit MIN_VALUE of zero, the MAX_VALUE is determined by the resolution

You stated that left-adjusting gives portability. If I have a 12bit DAC which expects left-adjusted values what would DAC.write(15) mean? I see two options here:

  • it's an exception (or the output voltage will be clamped to either 0V or 3.3V) because you intend to write a value outside the range of acceptable values (which are in [2**4, (2**16) - 1])
  • assuming that the parameter is of type uint16_t then DAC.write() must scale it's parameter like so dac_value = (value >> (16 - 12)) << (16 - 12). You further must write in the documentation that depending on the resolution of the underlying DAC several values will result in the same output voltage.

In short: Resolution is important.

I argue that the question of left versus right adjusting parameters for the DAC is orthogonal to the goal of portability. In order to reach portability DAC.write() must scale it's parameters with the unfortunate side effect that you have to properly document the pitfalls of auto-scaling and that code using DAC.write() must always take the performance hit of scaling.

The goal of my pull request is to unlock the full potential of 12bit DACs on the PyBoard.

So my proposal then will be to introduce a new method DAC.write_raw() which accepts uint16_t typed values and outputs right-adjusted. This requires the addition of a new paramater resolution_bits to DAC.__init__():

dac = DAC(pin, resolution_bits=12)
dac.write_raw(4095)

The resolution_bits parameter to DAC.__init__() can have whatever sensible default is required by (a more portable) DAC.write() such that we do not break existing code.
In other words if we choose the default to be 8 then code like:

dac = DAC(pin)
dac.write(255)

will still work like it used to. I would not touch the C code of the existing DAC.write() implementation and instead add code for a DAC.write_raw(uint16_t value).

@pfalcon
Copy link
Contributor

pfalcon commented Apr 30, 2015

I'd like to remind everyone that this is a pull request to unlock the full 12bit resolution of the DACs of the MCU on the PyBoard.

@ul5255 : Fair enough, but your change already breaks API, you asked why it's not progressing for months, and got that as a reply, and invitation from the maintainer to do consider how to break it in such a way that we don't need to break it again and again (while of course allow people to use it easily in Pythonic manner).

@pfalcon
Copy link
Contributor

pfalcon commented Apr 30, 2015

Instead of shifting left you could oversample.

Sorry, you can't do that "instead", those are completely orthogonal things.

@pfalcon
Copy link
Contributor

pfalcon commented Apr 30, 2015

what would DAC.write(15) mean? I see two options here:

Sorry, there's only one interpretation: Given constant, global for any port, documented value N, the above means to set output to a ratio of 15/2^N of full voltage range allowed by DAC.

The resolution_bits parameter to DAC.init() can have whatever sensible default is required by (a more portable) DAC.write() such that we do not break existing code.

Fair enough again. If @dpgeorge approves (I see no objective reasons why he wouldn't), patch for that is worth to be produced. My subjective complaint remains the same - I'd prefer to break API to allow to use it easily. If simple things can be done without configuration (like setting resolution, etc.), they should be done as such. More complex things may required config, that's ok.

@stinos
Copy link
Contributor

stinos commented Apr 30, 2015

I only briefly skimmed over this discussion and have been awake for too long so maybe I'm talking nonsense but: how about implementing write() in terms of write_raw() to which the result of a converter object is passed as argument? This converter is passed to the constructor and is set to some suitable default if not passed. Reasons to allow custom conversion: some users will want to do custom conversion anyway (e.g. to add dithering or to apply scaling based on calibration etc) and by adding it inside the DAC class it removes the need for having to scale twice (once by user to do whatever he/she desires and once again in DAC class itself), and also the problem of having to choose which sort of conversion is 'forced' upon all users (which seems to be one of the pain points in the discussion) is no more since users can supply their own without having to modify existing code.

@pfalcon
Copy link
Contributor

pfalcon commented Apr 30, 2015

@stinos: example of custom conversion: foo.write(my_func(value)). Nothing needs to be passed to constructor, etc. No need to duplicate and complicate what languages offers.

@stinos
Copy link
Contributor

stinos commented Apr 30, 2015

yes of course but that defeats my point of having to do only one conversion step - though I'm not sure it there are cases in which the small performance benefit of that is necessary

@pfalcon
Copy link
Contributor

pfalcon commented Apr 30, 2015

If you want performance, it should be done fixed-function in C. If you want flexibility, that's why there's Python. Need to have it half-way there, half-way there (actually, ratio if worse - you almost don't get performance, but lose flexibility and raise complexity) is rather questionable (pyboard's pyb module already duplicates Python mapping operation for GPIO pins - that may be seen as cute syntactic sugar, or as unneeded, non-orthogonal bloat (I follow 2nd opinion)).

@chrismas9
Copy link
Contributor

I do not see advantages of using left alignment but feel free to convince me.
Discussions related to ADC resolution, oversampling etc. should go someplace else.

You asked us to convince you about left justified data and one of the arguments is that all analog interfaces should have the same full scale value, eg 2^15, 2^24. Hence if there is a good reason for the ADC to be left justified to 15 or more bits then the DAC should be as well. ADC and DAC are complementary devices and are often used as a pair, eg audio record and playback. So I don't think the final DAC method should be defined without considering matching it to the ADC method. One of the arguments for having at least 15 bits for the 12 bit ADC is improving signal to noise ratio and the DAC interface should have the same 15 or more bits.

@chrismas9
Copy link
Contributor

Instead of shifting left you could oversample.
Sorry, you can't do that "instead", 

Both give the same result, but the oversampling method gives a more accurate or lower noise result. If you read a 12 bit ADC and shift left 3 bits you have multiplied by 8. If you take 8 readings at 2 MS/s they will essentially be the same value with small variations due to noise. Adding 8 same values together is the same as multiplying by 8. When you consider that most noise is Gaussian or asynchronous spikes (power supplies, motors etc) the noise component of the 8 samples tend to average out and give a value closer to the true analog input. If you expect 16384 for an analog input of Vref/2 the shift left method will likely give a value of between 16368 and 16400 (2 bits of noise). The oversampling method will likely give a value between 16376 and 16392 (1 bit of noise). For signals up to 100kHz bandwidth this gives a more accurate result with the only down side being the 4 usec sample time.

@ul5255
Copy link
Author

ul5255 commented Apr 30, 2015

Hence if there is a good reason for the ADC to be left justified to 15 or more bits then the DAC should be as well.

You do notice that ADC.read() is 12bit right-aligned right now, no? I mean look at my very first comment associated to the pull request: I say that with this pull request DAC.write() and ADC.read() would be aligned.

As for oversampling: This is so dependent on the application context (which MCU, board layout, EMI from environment, bandwidth required, ...) that I don't think one can come up with a generic way of doing it, much less in a base class like ADC.

dpgeorge added a commit that referenced this pull request Oct 13, 2015
This patch allows to configure the DAC resolution in the constructor and
in the init function, eg:

dac = DAC(1, bits=12).

The default resolution is 8 bits for backwards compatibility.  The bits
sets the maximum value accepted by write and write_timed methods, being
2**bits - 1.

When using write_timed with 12-bit resolution, the input buffer is
treated as an unsigned half-word array, typecode 'H'.

See PR #1130 for discussion.
@dpgeorge
Copy link
Member

This is finally implemented, see b5c43be.

You can set the resolution using DAC(1, bits=12) or dac.init(bits=12). 8-bits is the default if no bits parameter is given, for backwards compatibility.

DMA in 12-bit mode works: the input buffer is interpreted as an unsigned half-word array, ie array('H', [...]) with values going up to 4095. Some simple examples have been added to the docs.

Since we will eventually transition to the machine.DAC and machine.ADC classes, we can resolve the issue of portability later on.

@dpgeorge dpgeorge closed this Oct 13, 2015
@dpgeorge
Copy link
Member

@ul5255 thanks for the initial patch and sorry this took ages to get done.

tannewt added a commit to tannewt/circuitpython that referenced this pull request Aug 21, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants