Low Power Am2303 / DHT22 Driver for Propellor
tomcrawford
Posts: 1,129
Attached is a very low power driver the Aosong Am2303 Temperature Humidity module. It seems to be also called DHT22/23 at Adafruit. The driver takes a PASM cog, but will run reliably at 5MHz (standard crystal with pll1x.
Adafruit has a link to a datasheet; here is a link to a product manual that is actually intelligible.
http://www.aosong.com/asp_bin/Products/en/AM2303.pdf
The Am2303 has a one-wire interface. You have to provide a pullup resistor. You start it up by pulling the pin low for 1 msec or so and then high and then making it an input. The Am2303 then does a little handshake followed by 40 bits of pulse-width modulated data. The attached Saleae screen grab shows the handshake and the first 20 or so bits of data. The bottom two traces are instrumentation indicating when the cog is waiting for a bit to end and also indicating each time it found a "one".
The program has a spin segment that starts up the PASM. From time to time it sets a flag to allow the PASM cog to run and then displays the results. One interesting thing to note is that it returns temperatures below 0 degrees C as signed magnitude rather than one or twos complement as does the DS1620.
The PASM cog initializes itself by getting the pin number and forming a mask for dira, outa, and ina. It gets the hub addresses of its variables. It also receives a constant corresponding to the number of clock tics in 50 Usec. At label top, it checks once every 100 msec for its start flag to be set. Once it sees the flag set, it starts up the Am2303 and goes through the handshaking. It then accumulates and stores five bytes of eight bits each. It determines whether a bit is a one or a zero by comparing its high time to 50 microseconds. The S1Mask and S2Mask are purely instrumentation.
Note that the clock is programmed for pll1x. The program as shown runs at 2.1 mA. With the pst cog (and attendant calls) disabled, it runs at 1.5 mA. This seems suitable for battery operation.
Adafruit has a link to a datasheet; here is a link to a product manual that is actually intelligible.
http://www.aosong.com/asp_bin/Products/en/AM2303.pdf
The Am2303 has a one-wire interface. You have to provide a pullup resistor. You start it up by pulling the pin low for 1 msec or so and then high and then making it an input. The Am2303 then does a little handshake followed by 40 bits of pulse-width modulated data. The attached Saleae screen grab shows the handshake and the first 20 or so bits of data. The bottom two traces are instrumentation indicating when the cog is waiting for a bit to end and also indicating each time it found a "one".
{Demo PASM Driver for AOSONG Am2303}
CON
_clkmode = xtal1 + pll1x 'clock mode
_xinfreq = 5_000_000 '* crystal frequency
'ClkFreq is 5 MHz to reduce power
am2303pin = 12 'am2303 pin
'Define the LED
Led = 16
VAR
byte Debug, cog
long MyPar[6]
'these variables are associated with the am2303 and its cog
word RH, DegreesC, DegreesF 'relative humidity, Temperatures
byte Checksum, sumcheck 'returned checksum, calculated checksum
byte Am2303Data[5] 'the five bytes we read
byte Am2303Flag 'not equal to zero says read 2303, = 0 says data available
OBJ
pst : "parallax serial terminal"
PUB main
pst.Start (9600) 'start up ser terminal at 9600
pst.str(String("hello, world ")) 'runs in cog 1
pst.NewLine
waitcnt(clkfreq/10+cnt)
MyPar := @Am2303Data 'where to return the five bytes of data
MyPar[1] := Am2303Pin 'which pin to look at
MyPar[2] := @Am2303Flag 'flag that says start up the am2303
MyPar[3] := (clkfreq/1_000_000) * 50 'clock tics in 50 useconds '
cog := cognew(@Am2303Cog, @MyPar) 'start up the 2303 reader cog
pst.str(String("Am2303 Cog: ")) '
pst.hex(Cog,1)
pst.NewLine
repeat 'forever
waitcnt((clkfreq*10) + cnt) 'wait ten seconds
Am2303Flag := $55 'tell the temp cog to begin
waitcnt(clkfreq/200 + cnt) 'power down about 5 msec
repeat while Am2303Flag <> 0 'and wait here until data are returned
RH.byte[0] := Am2303Data[1] 'sort out returned data and calculate checksum
sumcheck := RH.byte[0]
RH.byte[1] := Am2303Data[0]
sumcheck := sumcheck + RH.byte[1]
DegreesC.byte[0] := Am2303Data[3]
sumcheck := sumcheck + DegreesC.byte[0]
DegreesC.byte[1] := Am2303Data[2]
sumcheck := sumcheck + DegreesC.byte[1]
Checksum := Am2303Data[4] 'checksum check
if checksum <> sumcheck
Pst.str(string("Checksum Error: Calculated: "))
pst.hex(Sumcheck,2)
Pst.str(string(" Received: "))
pst.hex(Checksum,2)
pst.newline
pst.str(string("Raw Temp Hex: "))
pst.hex(DegreesC,4)
pst.str(string(" DegreesC: "))
if (DegreesC & $8000) == 0 '0 degreesC or higher
pst.dec(DegreesC/10) 'integer part
pst.char(".")
pst.dec(DegreesC//10) 'tenths
DegreesF := ((DegreesC * 9) / 5) + 320 'degrees F times 10
DegreesF := (DegreesF + 5) / 10 'rounded
pst.str(String(" DegreesF: "))
pst.dec(DegreesF)
else 'below 0 degreesC
DegreesC := DegreesC & $7FFF 'drop the sign bit, leaving magnitude
pst.char("-") 'minus sign
pst.dec(DegreesC/10) 'integer part
pst.char(".")
pst.dec(DegreesC//10) 'tenths
DegreesF := 320 - ((DegreesC * 9) / 5)
DegreesF := (DegreesF + 5) / 10 'rounded
pst.str(String(" DegreesF: "))
pst.dec(DegreesF)
pst.str(string(" Raw Humidity Hex: "))
pst.hex(RH,4)
RH := (RH + 5) / 10 'round (up or down) and divide by 10
Pst.str(string(" Relative Humidity: "))
pst.dec(RH)
pst.newline
dat 'this is the cog that talks to the Am2303
Am2303Cog org 0
mov AdPar,Par 'get the address of input parameters
rdlong AdData, AdPar 'where to store the data
add AdPar, #4 'to pin number
rdlong AmPin, AdPar 'get it
add AdPar, #4 'to flag address
rdlong AdFlag, AdPar 'get it
add AdPar, #4 'to 50 usec count
rdlong Usec50, AdPar 'and get it
mov AmMask, #1 'form the mask for dira, outa
shl AmMask, AmPin 'by shifting appropriately
mov S1Mask, #1 'make the two scope sync masks
shl S1Mask, #14
or dira, S1Mask 'and enable the pins
mov S2Mask, #1
shl S2Mask, #15
or dira, S2Mask
top rdbyte MyFlag, AdFlag 'look at the START flag
test MyFlag, #$FF wz 'see if any bits set
if_nz jmp #start 'if so, go start the 2303
mov count2, TwoK 'otherwise set up to wait 100 millisec
top0 mov count1, Usec50
add count1, cnt
waitcnt count1, #0 'waiting in low power mode
djnz count2, #top0
jmp #top 'continue to wait
start mov count2, #20 '20 times 50 usec is 1 msec
or outa, AmMask 'dont glitch pin low
or dira, AmMask 'drive pin high
andn outa, AmMask 'now low. This starts the am2303 in 1 msec
loop1 mov count1, Usec50 '50 usec worth of clocks
add count1, cnt 'plus current time
waitcnt count1, #0 'wait for it
djnz count2, #loop1 'use up 1 msec
or outa, AmMask 'drive the pin high
andn dira, AmMask 'and get off the bus
waitpeq Zero, AmMask 'wait for Am2303 to take it low
waitpeq AmMask, AmMask 'and then back hi
mov CurAd, AdData 'where to store the first byte
mov Bytes, #5 'we will store five bytes
waitpeq Zero, AmMask 'and back low prior to first data bit
' 'we are now in the low time prior to first data bit
BytLp mov Bits, #8 'eight bits per byte
mov DByte, #0 'nice empty accumulator
BitLp shl DByte, #1 'shift what we have so far
or outa, S1Mask 'high while waiting for a bit to complete
waitpeq AmMask, AmMask 'wait for the beginning of the pulse
mov BCnt, cnt 'count at beginning of pulse
waitpeq Zero, AmMask 'wait for pulse to go away
mov ECnt, cnt 'count at end of pulse
andn outa, S1Mask 'bit is complete
sub ECnt, BCnt 'compute length of positive pulse
cmp ECnt, Usec50 wc
if_c jmp #BitLp1 'jump if length less than 50 use
or DByte, #1 'set a one
or outa, S2Mask 'scope pulse
nop
andn outa, S2Mask
BitLp1 djnz Bits, #BitLp 'eight bits
wrbyte DByte, CurAd 'save this byte
add CurAd, #1 'to next byte
djnz Bytes, #BytLp 'five bytes
wrbyte Zero, AdFlag 'we are done, zero the flag
jmp #top 'and go monitor the flag bit again forever
Zero long 0 'zero value for waitpeq
TwoK long 2_000 'two thousand (times 50 usec yields 100 msec)
AdPar RES 'the paramemters address
AdData RES 'the address of the first byte of data
CurAd RES 'the address to store next byte of data
AdFlag RES 'Address of the flag byte
AmPin RES 'the actual pin number
AmMask RES 'single bit corresponding to the Am2303 pin
Usec50 RES 'clock tics in 50 usec
count1 RES 'used for waitcnt's
count2 RES 'times out one msec
MyFlag RES 'my copy of the flag
S1Mask RES 'scope syncs
S2Mask RES
BCnt RES 'count at beginning of high pulse
ECnt RES 'count at end of high pulse
Bits RES 'counts bits in each byte
Bytes RES 'counts bytes of data
DByte RES 'where we accumulate each byte of data
The program has a spin segment that starts up the PASM. From time to time it sets a flag to allow the PASM cog to run and then displays the results. One interesting thing to note is that it returns temperatures below 0 degrees C as signed magnitude rather than one or twos complement as does the DS1620.
The PASM cog initializes itself by getting the pin number and forming a mask for dira, outa, and ina. It gets the hub addresses of its variables. It also receives a constant corresponding to the number of clock tics in 50 Usec. At label top, it checks once every 100 msec for its start flag to be set. Once it sees the flag set, it starts up the Am2303 and goes through the handshaking. It then accumulates and stores five bytes of eight bits each. It determines whether a bit is a one or a zero by comparing its high time to 50 microseconds. The S1Mask and S2Mask are purely instrumentation.
Note that the clock is programmed for pll1x. The program as shown runs at 2.1 mA. With the pst cog (and attendant calls) disabled, it runs at 1.5 mA. This seems suitable for battery operation.


Comments
Larry
Thanks for the code "Low Power Am2303 / DHT22 Driver for Propellor"
Could you do a modification for me.
I need the pin it is looking at to be modifiable within the running program, so I can monitor 2 RHT03's
Please help I really need it. "
Probably the easiest way would be to pass a second Am2303 pin number and a second Am2303Data address in the parameter list (dont forget to make the list bigger). Then duplicate the code from
Start through the last djnz #BytLp. You will also have to duplicate getting the values from the parameter list and forming the mask for the second Am2303 pin.
Better yet, If you have an extra cog available, start a second instantiation of the PASM cog, changing the pin number and Data address appropriately.
PS I tried to PM you but it said you weren't accepting emails.
Those two suggestions above are not very good.
Here is a much better idea: Instead of passing just a start command in Am2303Flag, pass the pin number. Am2303Flag := Am2303Pin | $80.
The hex 80 is in case the Am2303 pin is I/O zero. Then when you have processed the first sensor you can Am2303Flag := OtherAm23Pin | $80.
In the PASM code, at Start, change it to
Code:
start and MyFlag, #$1F 'keep just pin number
mov AmMask, #1 'form the mask each time you run
shl AmMask, MyFlag
mov count2, #20 'continue as before Everything else should be just the same
Thanks for the code, it works great!
mike