02-17-2021 05:17 PM
We have a ni 6229 that we would like to use to drive several heaters via mofsets. We would like to simply output pwm over selected digital lines. The 6229 is also using some python code to read from the analog inputs and write to the digital and analog outputs.
We found some documentation of pwm output using python nidaqmx. However this uses counter outputs (PFIs) which limits us two two ports. see Setting the Duty Cycle of a Counter Output Using NI-DAQmx for Python - National Instruments
I believe it is possible to set by sending arrays to the digital outputs similar to this labview example: NI-DAQmx: M Series Pulse Width Modulation (PWM) - NI Community However, there are two problems with this solution. 1) converting from the linked example into python nidaqmx is proving cumbersome, as the nidaqmx python documentation does not provide one-to-one functions for the linked example. 2) even if we get the example converted and running, it appears that we can only use the M-serries device with a single digital output task. The 6229 is currently using on-demand digital output, and the new array pwm program would need continuous output.
This is a long way of asking, Is there not a simple way to output PWM to digital lines on M-serries devices??? This is basic operation for a microcontroller.
02-17-2021 05:57 PM
actually, point 2) is not true. you can have multiple digital tasks. Maybe we will get around to this.
02-17-2021 06:57 PM
First, the *simple* way to put out PWM is with counter tasks. Doing it with a DO task will be possible, within some limits, but not exactly simple.
Second, I'm really no help with any of the Python syntax. I can only describe some things that should be doable, but not the details of exactly how to do it.
Third, a hardware-clocked, buffered DO task will necessarily have latency between when you write new data to the task buffer and when that data becomes a real-world output signal. There are ways to try to reduce latency, but if you push it too far you risk buffer underflow errors which will freeze all your output states, which is likely a worse outcome than the extra latency would have been.
Now, let's get down to business. Part of your latency is defined by the size of the task buffer you define when configuring your DO task. The buffer size is implicitly set by the amount of data you write to the task before starting it. So to keep latency low, write a smaller amount of data.
Fortunately, the default behavior of DAQmx in a continuous output task is to *regenerate* the buffer in a circular fashion. So when you don't need to change any of the PWM signals, you can just let the driver and hardware do their thing. But you need to, you can also update the buffer while the task is running to change the PWM. DAQmx will try to avoid "catching up and passing" the part of the buffer that's due to be transferred down to the board. Consequently, when you write a full buffer worth of new data, there can be as much as (buffer_size / sample_rate) seconds of latency before it all gets written. So it's at least that long until the next time you can change any of the PWM signals again.
There's also a hardware FIFO buffer on the device that adds to the latency. There are some DAQmx properties that let you try to control this to some extent, but the right thing to use varies with device family and with bus type (PCI or PCIe vs. USB).
At least as a thought experiment, I might aim for a task buffer of size 101. That lets you support any integer % PWM from 0-100 inclusive. Often, you might only need one of the output lines to change its PWM. But you still have to write to all of them, even if what you're writing to most of them is the same thing that was already in the buffer. So I'd keep a software 2D array variable containing N samples x M digital lines worth of data. Update whatever column(s) of that array you need to, then write it to the task.
In summary, yes it can be done and hopefully some of the things above give you a few breadcrumbs to follow while figuring out how to do it in Python. Doing PWM with DO has some limitations (latency, timing resolution, hassle of managing data arrays, etc.), but is probably feasible for a heat & temperature control app where system time constants are often longer than the data acq latency.
-Kevin P
02-17-2021 10:11 PM
Thanks kevin,
This is sort of what i was thinking, but its nice to hear confirmation it is possible. If we go this route, ill post a snipit here.
05-19-2021 10:41 PM
Looks like digital lines on our board cannot do buffered tasks.... I guess we will have to find another PWM solution
05-24-2021 07:15 PM
turns out only port0 can do buffered tasks. The following code snipit seems to get it done:
do_pwm_chans = [f'port0/line{n:d}' for n in range(8)]
self.do_pwm_chans=deepcopy(do_pwm_chans)
self.num_do_pwm_chan = len(do_pwm_chans)
if any(self.do_pwm_chans):
self.pwm_clk_task=nidaqmx.Task()#"pwm Sample Clk") make sure not used by clock task above
self.pwm_clk_task.co_channels.add_co_pulse_chan_freq(f'{self.deviceName}/ctr1', freq=1e7)
self.pwm_clk_task.timing.cfg_implicit_timing(samps_per_chan=2**11,sample_mode=AcquisitionType.CONTINUOUS)
self.pwm_samp_clk_terminal = f'/{self.deviceName}/Ctr1InternalOutput'
self.allTasks.append(self.pwm_clk_task)
self.pwm_clk_task.start()
self.pwm_task=nidaqmx.Task()#"Digital Output")
for ch in self.do_pwm_chans:
self.pwm_task.do_channels.add_do_chan(f"{self.deviceName}/{ch}",line_grouping=LineGrouping.CHAN_PER_LINE)
self.do_pwm_chans=[name.replace(self.deviceName+'/','') for name in self.pwm_task.channel_names]#make sure same order
self.pwm_task.timing.cfg_samp_clk_timing(int(1e6), source=self.pwm_samp_clk_terminal,active_edge=Edge.FALLING, sample_mode=AcquisitionType.CONTINUOUS)#,samps_per_chan=2047)
#self.pwm_task.timing.cfg_samp_clk_timing(int(1e6),active_edge=Edge.FALLING, sample_mode=AcquisitionType.CONTINUOUS)#,samps_per_chan=2047)
#self.pwmMultRead = DigitalMultiChannelWriter(self.pwm_task.out_stream)
self.allTasks.append(self.pwm_task)
n=2**13-2**11
bsize=2**13
self.pwm_task.stop()
self.pwm_task.write(np.array([np.append(np.zeros(bsize-n),np.ones(n)).astype(bool) for ch in self.do_pwm_chans]), auto_start=True)
#self.pwm_task.start()
05-27-2021 07:33 PM
while the above works, its isnt hardware timed and can clog up the bus. Implimenting this in python wasnt straight forward, but the below code appears to work and i think it is hardware timed. My card has an onboard buffer of 2047=self.pwm_buff
self.do_pwm_chans = [f'port0/line{n:d}' for n in range(4,8)]
if any(self.do_pwm_chans):
self.pwm_clk_task=nidaqmx.Task()#"pwm Sample Clk") make sure not used by clock task above
self.pwm_clk_task.co_channels.add_co_pulse_chan_freq(f'{self.deviceName}/ctr1', freq=1e6)
self.pwm_clk_task.timing.cfg_implicit_timing(sample_mode=AcquisitionType.CONTINUOUS)
self.pwm_samp_clk_terminal = f'/{self.deviceName}/Ctr1InternalOutput'
self.allTasks.append(self.pwm_clk_task)
self.pwm_clk_task.start()
self.pwm_task=nidaqmx.Task()#"Digital Output")
for ch in self.do_pwm_chans:
self.pwm_task.do_channels.add_do_chan(f"{self.deviceName}/{ch}",line_grouping=LineGrouping.CHAN_PER_LINE)
self.do_pwm_chans=[name.replace(self.deviceName+'/','') for name in self.pwm_task.channel_names]#make sure same order
self.pwm_task.timing.cfg_samp_clk_timing(int(1e6), source=self.pwm_samp_clk_terminal,active_edge=Edge.FALLING, sample_mode=AcquisitionType.CONTINUOUS,samps_per_chan=self.pwm_buffer)
#self.pwm_task.timing.cfg_samp_clk_timing(int(1e6),active_edge=Edge.FALLING, sample_mode=AcquisitionType.CONTINUOUS)#,samps_per_chan=2047)
self.pwmMultRead = DigitalMultiChannelWriter(self.pwm_task.out_stream,auto_start=True)
self.pwm_task.out_stream.output_buf_size=self.pwm_buffer
self.allTasks.append(self.pwm_task)
self.pwmMultRead.write_many_sample_port_uint32(np.array([np.append((2**32-1)*np.ones(1547),np.zeros(500)).astype(np.uint32),
np.append((2**32-1)*np.ones(1547),np.zeros(500)).astype(np.uint32),
np.append((2**32-1)*np.ones(347),np.zeros(1700)).astype(np.uint32),
np.append((2**32-1)*np.ones(347),np.zeros(1700)).astype(np.uint32)]))