Author: Erich Wälde Contact: firstname.lastname@example.org Date: 2015-04-19
- Nodes on a RS485 Bus
- 1 Abstract
- 2 Motivation: The Project “collector”
- 3 Hardware Requirements
- 4 Software Requirements
- 5 Implementation Plan
- 6 Code
- 6.1 start
- 6.2 making
prompt_readya deferred word
- 6.3 stationID
- 6.4 emit-on-off
- 6.5 adding rs485 r/w handling
- 6.6 mpc 1: making usart_rx_isr a forth level word
- 6.7 mpc 2: adding mpc after all
- 6.8 going to quiet mode on unparsable input
- 6.9 turnkey
- 7 creating a node
- 8 The somtimes-not-so-obvious things
- 9 Finally
- 10 References
The cookbook style recipes presented below are interconnected by the desire to create a solution connecting several controller nodes together by rs485 network for periodic data collection. A few decisions have been taken upfront:
- The rs485-bus is wired up as “simplex” (not duplex).
- There is exactly one node on the rs485 network acting as bus-master (the data collecting Linux system) and initiating any data transfer.
- Every node has to exlicitly switch its bus transceiver to “send” mode when writing data onto the bus.
- There must be a means to address one node whereas all the other nodes must remain silent.
Substantial parts of this solution were inspired or recycled from a project published by Lubos Pekny.
The author is running a set of currently 5 data collecting controller nodes on one rs485 bus for several years now. The code provided stable operation so far.
For this article the author decided to “redo everything” in plain amForth, well almost everything, as it turned out.
When starting with microcontrollers years ago I needed to do something with them, that looked useful at least to me. So very soon I started to deal with sensors to measure temperature, pressure, and humidity of air. The classical “weather station” project. This worked, but soon I wanted to have the values not only displayed but rather collected — some day nice graphs would be interesting, or so I thought. So I connected a small single-board-computer  to the only one controller with a serial cable. This worked for a long time.
Later I had the idea to collect temperatures at other points in the house as well. I could have added more sensors and long cables to the only one controller, but instead I decided to add another controller at the end of a long cable. Now I needed a way to talk to two (or more) controllers connected “somehow” the the single board computer. I could have added more serial interfaces, one for each controller, but I didn’t like the idea. Instead I decided to connect two (or more) controllers via one long cable using the RS485-Bus specification.
The RS485-Bus is an electrical specification. A (possibly twisted) pair of wires is used to connect two nodes. The signal is differential, the difference in potential between the two wires is used as information. That way the whole setup is fairly immune against noise, as this normally adds the same shift in potential to both wires. The standard is good for a distance up to 1200 m, but should work much longer distances with slow data rates and proper termination.
In order to exchange information on the RS485-bus the same timing and encoding is used as on a normal serial interface (RS232). The idle levels are interpreted as 1, the first bit is the start bit and always 0, then 8 data bits follow, and then one or more stop bits are sent. The stop bits are also 1 and correspond to the idle levels. So there is nothing new here.
However, if several nodes are on the bus, to which one am I talking? And if it is the wrong one, how do I “address” the correct one? In order to distinguish addresses (or control bytes) from ordinary data, some agreement has to be established, for example
- Bytes with the most significant bit set are treated as addresses or control bytes
- Bytes with the most significant bit cleared are treated as ordinary data
If the bytes
0x00 .. 0x7f (the lower half of the ascii table) are
sufficient for all data exchange, then 8 data bits are still good. If
not, 9 data bits can be used in many cases. AVR controllers provide
that possibility. There is a project using 9bit communication on Linux
as well (uLan), Links section.
In my case I decided to transfer all information as ascii strings, e.g.
7F is the station address (stationID) in hex,
01 is the
sensor number on that node (also in hex), and after the colon a list of
4 numerical values, their precise meaning being entirely irrelevant at
this point. The main advantage is that I can just read everything on
the bus in clear with little technical overhead.
Currently I run a set of 5 controllers with a variety of sensors:
- temperature and humidity (indoors and outdoors), pressure of air
- voltage of an accumulator providing power to a remote system
- distance (ultrasonic range finder) which translates to the amount of water in a tank
- counts of meters (electricity, water, natural gas)
The “collector” is a perl script running on the single board computer and collecting the data from the controllers every 2 or 10 minutes. This program acts as the bus master, the other nodes do not initiate any data exchange. The data is then accumulated in a sqlite database. A “viewer” perl script will then produce graphs of selected values over time. Other options are possible, of course.
All controller nodes need to have a RS485-transceiver. The transceiver needs 3 connections to the controller:
- TX –> Data Out
- RX <– Data In
- Port D7 –> Write/Read select (idle = read = low)
An RS232 – RS485 converter of some sort is needed to connect the serial interface of the collector computer to the bus. USB – RS485 dongles are available as well. Please note, that the connector should not produce a local echo of the bytes transmitted, or software needs to take care of the double echo. The controllers are sending an echo character as well, this serves as handshake when uploading forth code, too.
Power supply can be local to any node, but serving power on 2 more wires is also possible. When having long cables +12V supply voltage and step down converters on every board seem like a good idea.
- Any node should be quiet and not write anything to the bus unless
explicitly requested to do so. This explicit request translates into
some sort of addressing. Preventing any output is most easily
achieved by changing
0x00 .. 0x7fare considered normal data, bytes
0x80 .. 0xffare considered adresses (or control bytes).
- every node needs to have an address or stationID assigned
- The controllers make use of the so called multi processor communication (MPC) mode to ignore traffic between other nodes already in hardware.
- a write operation to the bus must assert the W/R pin to write before transmission.
- Upon completion of the (asynchronous) data transmit the W/R pin has to be released, e.g. automatically by using the transmit complete interrupt.
- The implementation should be in Forth entirely. A few exceptions showed up during implementation.
- If a node is power cycled, nothing weird should happen on power up. Especially nothing should be written to the bus at all.
- a modified prompt shall include the address (stationID) of the node at the other end of the communication. This is solely to provide immediate visible feedback, it is not needed for proper communication.
- While experimenting it turned out that sometimes more than one controller is in normal mode. They will produce what I call “echo loops”. The output (mostly error messages) of one node will trigger more output (error messages) of the other node. I decided to implement harsh measures: whenever command line will produce an error message, then instead put the node back to quiet mode.
In order to achieve the above goals, a set of mutually independant things were implemented.
This is to satisfy requirements 3 and 9. The code is fairly simple, even short.
stationID is an eeprom backed value with a cache place in RAM.
The prompt itself is produced by p_rd. Currently this is not a deferred word and therefore cannot be overridden easily by another function. So we make it a deferred word (assembly change 1) and then implement a new function p_id_rd which will then be registered into the deferred function p_rd.
-emit / +emit
In order to prevent any output from the controller, I chose to make emit point to drop rather than tx. -emit will take care of this. This word will be called in the next part at startup time.
rs485 read/write pin handling
One pin on the controller has to be selected to drive the read/write-pin of the transceiver. The idle state should be low (0) or read, which is achieved by a pull-down resistor.
- provide constants to declare the selected pin
- switch the pin to output on startup
- set the pin to write (1) before transmission
- release the pin to read (0) upon completion. Use the transmit complete interrupt to achieve this.
We can now write to and read from the RS485 bus. We can handle only one controller so far.
mpc — multi processor communication
This is the most complex part.
- set quiet mode set the serial interface to 7N2 (7 data bits, no parity bit, 2 stop bits), set the MPCM0 bit in register UCSR0C
- when receiving a byte with the most significant bit set, inspect the byte and decide whether this is the local address or not
- if not, remain in quiet mode
- if yes, then switch the serial interface to normal mode (8N1) and handle all incoming data
- set normal mode: set the serial interface to 8N1, clear the MPCM0 bit in register UCSR0C
It turned out that the function usart_rx_isr is implemented in assembly and registered as a low level interrupt service routine (ISR). This prevents overriding the registered interrupt service routine with another function. I decided to change this and make usart_rx_isr a forth level routine (assembly change 2) and register it as a high level interrupt. This way the ISR can be replaced by some other function.
Unsurprisingly replacing the ISR needs explicit access to the ring buffer that the original ISR is using. It is accessed by key as well and should not change. So I added forth level headers to make the space in RAM available as forth constants/variables (assembly change 3)
recognizer: go quiet if command not found
amForth provides recognizers. There is a list of them, which can be changed. The first in the list is rec:word, it will try to find the token in the word list. If it fails, the next one in the list is called: rec:num. It will try to parse the input token as a number. If it fails the list is exhausted and the final r:fail will be called to issue an error message and do some cleanup.
I decided to add a third recognizer to the end of the list named rec:quiet. It will not parse the input token again, but clean up the arguments. Then it will set the controller to quiet mode (call -emit +mpc7) and signal success rather than error. This way the pointer in r:fail is not called.
startup / turnkey
In the end all of the above things need to be put together to ensure correct startup and initialization of all parts involved. Pay attention to turnkey and power cycle.
This code was re-developed and tested on an atmega644p running amForth 5.5.
The remainder of this article assumes that we have a working setup derived from the
directory. Set appropriate values for the controller type, crystal frequency, and baud rate to appropriate values for your board.
.equ F_CPU = 11059200 .set BAUD=115200
Now we are at the point where the controller should talk to us on the serial interface using a terminal program, e.g. minicom:
Welcome to minicom 2.7 OPTIONS: I18n Compiled on Jan 1 2014, 09:30:18. Port /dev/ttyUSB1, 16:46:00 Press CTRL-A Z for help on special keys amforth 5.5 ATmega644P >
So the next iteration will make p_rd a deferred word — currently it is not. So in the current working directory we make local copies of
to prepare the change. In the include order prepared for the call of the
assembler, files in the local directory are preferred over those of the
current AMFORTH directory, pointing to
So first we add space in eeprom to keep the current execution token (XT) of
p_rd right at the end of file
initial value points to the original function.
diff --git a/02_doc_rs485/amforth-eeprom.inc b/02_doc_rs485/amforth-eeprom.inc index 8403522..62aece3 100644 --- a/02_doc_rs485/amforth-eeprom.inc +++ b/02_doc_rs485/amforth-eeprom.inc @@ -67,3 +67,5 @@ EE_INITUSER: .endif EE_UBRRVAL: .dw UBRR_VAL ; BAUDRATE +EE_PROMPT_RDY: + .dw XT_PROMPTRDY_INT
Then we change p_rd to a deferred word, and the original code to (p_rd).
diff --git a/02_doc_rs485/words/prompts.asm b/02_doc_rs485/words/prompts.asm index 8f0e945..c5a9472 100644 --- a/02_doc_rs485/words/prompts.asm +++ b/02_doc_rs485/words/prompts.asm @@ -1,14 +1,28 @@ +; make prompt_ready a deferred word + +VE_PROMPTRDY: + .dw $ff04 + .db "p_rd" + .dw VE_HEAD + .set VE_HEAD = VE_PROMPTRDY +XT_PROMPTRDY: + .dw PFA_DODEFER1 +PFA_PROMPTRDY: + .dw EE_PROMPT_RDY + .dw XT_EDEFERFETCH + .dw XT_EDEFERSTORE + ; ( -- ) ; System ; send the READY prompt to the command line -;VE_PROMPTRDY: -; .dw $ff04 -; .db "p_rd" -; .dw VE_HEAD -; .set VE_HEAD = VE_PROMPTRDY -XT_PROMPTRDY: +VE_PROMPTRDY_INT: + .dw $ff06 + .db "(p_rd)" + .dw VE_HEAD + .set VE_HEAD = VE_PROMPTRDY_INT +XT_PROMPTRDY_INT: .dw DO_COLON -PFA_PROMPTRDY: +PFA_PROMPTRDY_INT: .dw XT_CR .dw XT_DOSLITERAL .dw 2
With that in place the appearance of the prompt can be changed if we so desire:
amforth 5.5 ATmega644P > : new_p_rd cr ." --new> " ; ok > ' new_p_rd is p_rd ok --new> 1 3 + . 4 ok --new> ' (p_rd) is p_rd ok >
This will be used in the next step to display the content represented by stationID in the ready-prompt.
StationID is a value, permanently stored in EEPROM and copied to a RAM location on startup. So we need to load the appropriate word Evalue:
After that we are able to create a value, the content of which is backed in EEPROM:
$007f Evalue stationID
With this in place we are now in a position to create a new function
implementing a new prompt. In order to make it always look the same (two
digits, leading zeros) we add the word u0.r to the dictionary in
dict_appl.inc (please note the leading dot and the quotes, since this
is assembly syntax):
reassemble and reflash amForth. The define the new word p_id_rd
: p_id_rd cr base @ hex [char] ~ emit stationID 2 u0.r [char] > emit space base ! ;
We should also take care to save and restore the content of base, since I decided to print out the value of stationID in hexadecimal. Using the new things should work like this:
amforth 5.5 ATmega644P ok > stationID decimal . 127 ok > p_id_rd ~7F> ok > ' p_id_rd is p_rd ok ~7F> $42 to stationID ok ~42>
$007F is the highest address available for the above
mentioned 7-bit addressing scheme, so I chose it as the default. The exact
value can be changed here or overwritten later when loading the code with
$42 to stationID
In order to prevent the controller from writing to the rs485 bus unless explicitly requested, I decided to defer emit to drop just to make sure. This requires two fairly simple words
variable old-emit ' emit defer@ old-emit ! : -emit ['] emit defer@ old-emit ! ['] drop is emit ; : +emit old-emit @ is emit ;
After loading the code we can test this:
amforth 5.5 ATmega644P ok ~42> ~42> : hi ." howdy, mate!" cr ; ok ~42> hi howdy, mate! ok ~42> -emit hi +emit ok
In order to drive the rs485 transceiver, we need to implement the following things:
- select W/R pin
This pin needs to be selected, initialized as output and set to low.
$2B constant RS485_PORT \ memory mapped $2A constant RS485_DDR \ . $80 constant RS485_PIN_MASK : rs485-pin-output RS485_DDR c@ RS485_PIN_MASK or RS485_DDR c! ;
Of course the functions in
lib/bitnames.frt could be used as well,
but for the argument of smaller dependencies, I decided to implement
- set W/R pin high (write) or low (read)
Two simple functions will do this:
: rs485-write RS485_PORT c@ RS485_PIN_MASK or RS485_PORT c! ; : rs485-read RS485_PORT c@ RS485_PIN_MASK invert and RS485_PORT c! ;
set W/R pin to write (1) before sending a byte
Before sending any byte, we need to set the W/R pin high. So we reimplement tx-poll, the function that transfers one byte to the serial interface.
$C6 constant UDR0 \ usart0 data register : rs485-tx-poll ( c -- ) begin tx?-poll until rs485-write UDR0 c! ;
release W/R pin upon transfer completion
After sending the byte, the W/R pin should be released to zero. This happens some time after initiating a transfer. However, the Atmel engineers have anticipated this problem and provided the transfer complete interrupt for our convenience.
: tx-complete-isr RS485_PORT c@ RS485_PIN_MASK invert and RS485_PORT c! ; $2C constant USART0__TXAddr \ USART0, Tx Complete $40 constant UCSR0B_TXCIE0 $C1 constant UCSR0B : +rs485 rs485-pin-output rs485-read ['] tx-complete-isr USART0__TXAddr int! ['] rs485-tx-poll is emit UCSR0B c@ UCSR0B_TXCIE0 or UCSR0B c! ; : -rs485 ['] noop USART0__TXAddr int! ['] tx-poll is emit UCSR0B c@ UCSR0B_TXCIE0 invert and UCSR0B c! ;
The functions +rs485 and -rs485 enable and disable the whole rs485 bus connection. Apart from changing the deferred word emit and registering the interrupt service routine to the transfer complete interrupt, the interrupt itself must be enabled in the register
At this point we have everything in place to connect to the controller via the rs485 bus. +rs485` needs to be called during startup, which is the only missing piece at this point.
While working on this particular implemention of my code, namely reimplementation in as much Forth code as possible, I came across a subtle feature of the amForth implementation (as of version 5.5). Interrupt handling in amForth is twofold: the low level part (written in assembly) is basically doing the bookkeeping, clearing the interrupt and then calling into a amForth table of registered functions. This provides the possibility to write interrupt service routines (ISR) in high level Forth rather than assembly. Registering your own ISR is a matter of one line:
' your-own-isr Interrupt-Vector-Addr int!
However, it turned out that the interrupt service routine for receiving
bytes from the serial interface was not constructed in this way but
registered directly as a low level ISR, bypassing the process outlined
above. It took me some head scratching to find out, of course, but it
provides an opportunity to better understand the inner workings of amForth
as well. Therefore I decided to reimplement the default
usart_rx_isr as a Forth function, which is then registered to
the receive complete interrupt (
URXC0) in applturnkey. With
this change in place, we can easily register an mpc-specific ISR to handle
The affected code is found in file
The original assembly function
usart_rx_isr: push xl in xl, SREG push xl push xh push zl push zh lds xh, USART_DATA usart_rx_store: lds xl, usart_rx_in ldi zl, low(usart_rx_data) ldi zh, high(usart_rx_data) add zl, xl adc zh, zeroh st Z, xh inc xl andi xl,usart_rx_mask sts usart_rx_in, xl usart_rx_isr_finish: pop zh pop zl pop xh pop xl out SREG, xl pop xl reti
is registered as interrupt service routine for
.set usartpc = pc .org URXCaddr jmp_ usart_rx_isr .org usartpc
So we need three parts to implement the desired change:
- replace usart_tx_isr with a Forth word
- remove the registration of the original asm function
- register the new function as ISR
This part is not particularly difficult, because a Forth equivalent is found already as a comment in the asm file.
; forth code: ; : rx-isr USART_DATA c@ ; usart_rx_data usart_rx_in c@ dup >r ; + ! ; r> 1+ usart_rx_mask and usart_rx_in c! ; ; ; setup with ; ' rx-isr URXCaddr int!
So all we need to do is to shape this into a Forth word:
; --- make usart_rx_isr a forth function, needs to be registered in applturnkey ; --- could be calling the original asm code instead ... VE_USART_RX_ISR: .dw $ff0c .db "usart_rx_isr" .dw VE_HEAD .set VE_HEAD = VE_USART_RX_ISR XT_USART_RX_ISR: .dw DO_COLON PFA_USART_RX_ISR: .dw XT_DOLITERAL ; UDR0 c@ .dw USART_DATA .dw XT_CFETCH ; usart_rx_data usart_rx_in c@ dup >r .dw XT_DOLITERAL .dw usart_rx_data .dw XT_DOLITERAL .dw usart_rx_in .dw XT_CFETCH .dw XT_DUP .dw XT_TO_R ; + ! \ ? c! .dw XT_PLUS .dw XT_CSTORE ; r> 1+ usart_rx_mask and usart_rx_in c! .dw XT_R_FROM .dw XT_1PLUS .dw XT_DOLITERAL .dw usart_rx_mask .dw XT_AND .dw XT_DOLITERAL .dw usart_rx_in .dw XT_CSTORE .dw XT_EXIT
I kept the name, but please note that it does not refer to the asm label any more — usart_rx_isr is now a proper Forth word.
This part is very easy, we just remove the 4 lines doing the registration by commenting them out:
; --- do NOT register "usart_rx_isr:" as low level isr! ; .set usartpc = pc ; .org URXCaddr ; jmp_ usart_rx_isr ; .org usartpc
The new function must be registered somewhere in the startup of amForth, because otherwise there will be no access to the command loop via the serial interface. So in function applturnkey we add the equivalent of
' usart_rx_isr USART0__RXAddr int!
just before globally enabling interrupts.
; ( -- ) System ; R( -- ) ; application specific turnkey action VE_APPLTURNKEY: .dw $ff0b .db "applturnkey",0 .dw VE_HEAD .set VE_HEAD = VE_APPLTURNKEY XT_APPLTURNKEY: .dw DO_COLON PFA_APPLTURNKEY: .dw XT_USART ; register usart_rx_isr .dw XT_DOLITERAL ; ' usart_rx_isr URXCaddr int! .dw XT_USART_RX_ISR .dw XT_DOLITERAL .dw URXCaddr .dw XT_INTSTORE .dw XT_INTON .dw XT_VER .dw XT_EXIT
Assembling amForth and programming the controller with these changes must result in an equally usable system as it was before.
Entering MPC mode in this case means configuring the serial interface
to 7N2 (7 data bits, no parity bit, 2 stop bits) and setting the
MPCM0 bit in register
In that mode, if a data frame is received with the most significant bit cleared (0), the the data frame is silently ignored.
In that mode, if a data frame is received with the most significant bit set
(1), then the data frame shows up in register
UDR0, the data register
of the serial interface. An interrupt is generated and the corresponding
ISR is called.
All nodes on the bus will inspect the just arrived address byte. If the
value of the address byte is the same as the configured node address (also
known as stationID), only then the serial interface is
reconfigured to 8N1 and the
MPCM0 bit is cleared. This node is then
awake from a communication point of view. It will receive all following
data frames and is expected to act on them.
All other nodes on the bus will keep the 7N2 mode of the serial interface and remain silent from a communication point of view.
The awake state will not end and must be changed explicitly.
Things that need to be done are
provide a few definitions for readability (recycled from
devices/$(MCU)/$(MCU).frt— make sure to load the correct file for your controller!)
$2C constant USART0__TXAddr \ USART0, Tx Complete $28 constant USART0__RXAddr \ USART0, Rx Complete $40 constant UCSR0B_TXCIE0 $C0 constant UCSRA \ UCSR0A, really $10 constant UCSRA_FE0 \ frame error $08 constant UCSRA_DOR0 \ data over run $04 constant UCSRA_UPE0 \ parity error $01 constant UCSRA_MPCM0 \ mpc mode enabled $C1 constant UCSR0B $C2 constant UCSRC $C6 constant UDR0
waiting for the currently active transfer to complete (reusing definitions from the rs485 section above)
: txc begin RS485_PORT c@ RS485_PIN_MASK and 0= until ;
This is needed whenever we want to switch to mpc mode. Without waiting we will destroy any ongoing transmit.
enabling MPC mode (7N2)
: +mpc7 txc $0C UCSRC c! \ 7N2 UCSRA c@ $01 or UCSRA c! \ MPCM0=1 ;
disabling MPC mode (8N1)
: -mpc7 ( -- ) UCSRA c@ $01 invert and UCSRA c! \ MPCM=0 $06 UCSRC c! \ 8N1 ;
access to the RX data ring buffer
Handling incoming data unfortunately requires access to the variables of the rx ring buffer, which are not readily available in forth. In a local copy of
drivers/usart-isr-rx.asmwe add appropriate provisions. The existing declaration of the used RAM space and sizes
; sizes have to be powers of 2! .equ usart_rx_size = $10 .equ usart_rx_mask = usart_rx_size - 1 .dseg usart_rx_data: .byte usart_rx_size+2 usart_rx_in: .byte 2 usart_rx_out: .byte 2 .cseg
will be made available as amForth constants and variables.
\ variable USART_RX_DATA N allot \ &buffer \ variable USART_RX_IN \ index \ N 1- constant USART_RX_MASK \ length-1, length=2^n ; ( -- value ) constant USART_RX_DATA VE_USART_RX_DATA: .dw $FF0D .db "USART_RX_DATA",$00 .dw VE_HEAD .set VE_HEAD = VE_USART_RX_DATA XT_USART_RX_DATA: .dw PFA_DOVARIABLE PFA_USART_RX_DATA: .dw usart_rx_data ; ( -- addr ) variable USART_RX_IN VE_USART_RX_IN: .dw $ff0b .db "USART_RX_IN",$00 .dw VE_HEAD .set VE_HEAD = VE_USART_RX_IN XT_USART_RX_IN: .dw PFA_DOVARIABLE PFA_USART_RX_IN: .dw usart_rx_in ; ( -- value ) constant USART_RX_MASK VE_USART_RX_MASK: .dw $FF0D .db "USART_RX_MASK",$00 .dw VE_HEAD .set VE_HEAD = VE_USART_RX_MASK XT_USART_RX_MASK: .dw PFA_DOVARIABLE PFA_USART_RX_MASK: .dw usart_rx_mask
This provides the words USART_RX_DATA USART_RX_IN USART_RX_MASK for our usage. Alternately we could setup our own variables and replace rx-isr with a version looking at them.
handling an incoming byte according to MPC-mode
UCSRA_FE0 UCSRA_DOR0 or UCSRA_UPE0 or constant UCSRA_RX_ERR : mpc? UCSRA c@ UCSRA_MPCM0 and ; : rx-err? UCSRA c@ UCSRA_RX_ERR and ; : rx-store ( udata -- ) USART_RX_DATA USART_RX_IN c@ dup >r + ! r> 1+ USART_RX_MASK and USART_RX_IN c! ; : mpc-rx-isr rx-err? 0= if UDR0 c@ \ -- udata mpc? if stationID = if -mpc7 then else rx-store then then ;
The new word command:mpc-rx-isr will inspect incoming data according to whether we are in MPC mode or not. It requires the node address in the value stationID as defined before.
string everything together
In order to use all of the above we basically need to switch it on (and off):
: +rs485.mpc ['] prompt_rd is p_rd \ overwrite p_rd +rs485 ['] mpc-rx-isr USART0__RXAddr int! \ overwrite usart_rx_isr -emit +mpc7 ; : -rs485.mpc ['] (p_rd) is p_rd ['] usart_rx_isr USART0__RXAddr int! -rs485 -mpc7 +emit ;
When using this in a turnkey word, make sure to disable emit before calling the original word applturnkey, because otherwise the output of ver will be written to the bus.
: run-turnkey -emit applturnkey +rs485.mpc \ more initialization here \ begin \ your periodic work goes here \ again ;
We are all ready to go. Please note that you need some means to send
0x80 | 0xStationAddress to the bus to address the desired node. Once
connected you need to issue +emit, and only after that the
ok-prompt will show up.
After everything worked thus far I found out, that sometimes more than one controller on the bus will be awake receiving data and acting on it. Most of the time this would result in error messages being sent to the bus, which in turn will create another round of error messages. I called this the echo loop. I did not find out, what really caused this behaviour, but instead I decided: whenever a node receives illegible input that cannot be handled properly, the node shall return to mpc quiet mode and not write any error messages at all.
The desired behaviour is a fairly fundamental change to the command loop, however, it is easy to install thanks to the availability of recognizers.
Any input will be parsed by a list of recognizers, the first to understand the input will trigger the corresponding work. The last in the list will be the one to possibly issue an error message. So we create a new recognizer and insert it into the list of recognizers before the one issueing error messages.
First we need to load the word recognizer:
After that we create a table holding 3 execution tokens. The first is to be called at runtime, the second at compile time, and the third during a postpone operation.
:noname -emit +mpc7 ; \ at runtime call the equivalent of ~end ' noop \ nothing to do at compile time :noname type -48 throw ; \ postpone would be an error recognizer: r:quiet
The parsing word does basically nothing. If this recognizer is called, rec:word and rec:num have not been able to handle the input. So we simply drop the references to the unhandled input before the call into an entry of the newly created table r:quiet.
: rec:quiet ( addr length -- t/f ) drop drop r:quiet ;
Registering and deregistering the new recognizer is a little involved, because we want to place it at the last position — if the last recognizer fails, the content of r:fail is called. After some fiddling, I decided to compare the last value with the one to be inserted or removed, such that repeated calls to “+rec:quiet” or “-rec:quiet” will not cause a problem.
: +rec:quiet ['] rec:quiet \ -- r0 get-recognizers \ -- r0 r1 r2 2 dup pick \ -- r0 r1 r2 2 r1 ['] rec:quiet <> if \ -- r0 r1 r2 2 1+ set-recognizers else set-recognizers \ 0 ?do drop loop \ rather? no change? drop then ; : -rec:quiet get-recognizers \ -- r0 r1 r2 3 dup pick ['] rec:quiet = if 1- set-recognizers drop else 0 ?do drop loop then ;
+rec:quiet needs to be called in +rs485.mpc and similar for -rec:quiet.
$28 constant USART0__RXAddr : +rs485.mpc ['] prompt_rd is p_rd \ overwrite p_rd +rs485 ['] mpc-rx-isr USART0__RXAddr int! \ overwrite usart_rx_isr +rec:quiet -emit +mpc7 ; : -rs485.mpc ['] (p_rd) is p_rd ['] usart_rx_isr USART0__RXAddr int! -rec:quiet -rs485 -mpc7 +emit ;
We are done. We can now put this together in a function to be called at system boot. The controller will immediately switch off any output and go to quiet mpc mode. As such the controller will behave well on a bus with possibly other nodes.
: run-turnkey -emit applturnkey +rs485.mpc ;
Please note that -emit must be called before applturnkey, because the later does call ver producing the well known output
amforth 5.5 ATmega644P ok
or similar. But we do not want to write anything on the bus unless explicitly asked to do so.
While the above implementation is complete, it may not be obvious, how to create a sensor node with all the required bits around it. So at least the description of a working example seems needed.
In my case the
collector is a perl script, which will periodically
address a list of nodes and for each of these
write the address byte
0x80 | addrto the bus
write +emit after that (no echo characters expected)
wait for the ok prompt
write ~data to the bus (waiting for each echo character, since those are coming from the controller now)
read all the characters which come as an answer, e.g.
__Q 42:0005 4200:0 4201:0 4202:0 4203:0 C-- ok
__Q 7F:0005 7F01:3,+19.50,+19.50,+19.50 7F02:3,514,516,518 C-- ok
write ~end to the bus (again waiting for each echo character)
The answer string is then parsed into pieces, and individual measurements are then inserted into a database table.
C-- tokens were inserted only to make parsing simpler.
The second token consists of
stationID:softwareVersion, both as a hex
number. Tokens after that are either
sensorID:N,xlow,xmean,xhigh collections. Other formats are certainly
possible, this is just my choice based on the decision its all plain ascii.
This represents the high level view of the node as seen from the network (aka bus). So the words
must be available on the node.
So there are at least two ways to make ~data report meaningful output.
interrupt only sensors
If all sensors can be handled by appropriate interrupt service routines, those shall fill the variables with meaningful values. ~data will then only read those values and report them over the bus.
This setup is used for counters or meters. In my case the electricity meter has a so called S0 interface with two pins
+must be pulled high by a pullup resistor and connected to a controller pin. The meter will short the
-pin for a few milliseconds thus reporting one count. If the pin at the controller either can react on such a pulse by issueing an interrupt (external or pin change interrrupt) or if the pin is connected to a counter register, that’s all there needs to be done. Every low pulse will increment the value reported by ~data.
using the multitasker to do the work in the background
If there is more work to be done, either on event or periodically, then using the multitasker is an option. There are only two tasks involved: the task serving the command line and the task periodically collecting sensor readouts into variables. The handling of sensors or events could be spread over more tasks, if needed for some reason.
If ~data is called on the command line, it will report the stored values and optionally reset the variables.
As an example I will outline the needed bits for a counter node. It
will count active-low pulses on one of 4 pins. The controller is an
atmega168, which features pin-change-interrupts on all port pins. The
pulses are produced by an electricity meter with a so called S0
interface. This particular electricity meter will produce 1000 counts
per kWh consumed, each count consists of pulling pin
+ down for
The pin change interrupt will trigger on falling and rising edges. There is only one interrupt for a group of eight pins (one port). So the interrupt service routine needs to find out, which pin exaclty triggered the interrupt, and whether a falling or rising edge did occur. On the falling edge we need to increment the associated counter for this pin.
\ --- data handling ------------------------- variable Count 4 cells allot variable Pins_old : pci1_isr ledsensor high PINC c@ $0F and \ -- pins Pins_old c@ \ -- pins alt over \ -- pins alt pins xor \ -- pins diff dup if \ -- pins diff 4 0 do \ for each (consequtive input) pin dup 1 i lshift and if \ . bit one changed? over 1 i lshift and 0= if \ . leading edge? 1 Count i cells + +! \ . increment then then loop then ( diff ) drop ( pins ) Pins_old c! ledsensor low ; : +pci1 $0F PCMSK1 c! \ pcint 8..11 active $02 PCICR c! \ pci1 active $02 PCIFR c! \ clear PCI1, just in case PINC c@ $0F and Pins_old ! ['] pci1_isr PCINT1Addr int! ; : -pci1 $00 PCICR c! $02 PCIFR c! \ clear PCI1, just in case ;
The function ~data will then read the counter and report the value found as a plain ascii string on the serial interface. No provisions are taken to implement any access locking, reading the two bytes of the counter might result in inconsistent values.
\ counters are expected signed. A rollover can then be detected \ and distinguished from restart of the controller. \ therefore '.' not 'u.' in data.ls : data.ls 4 0 do space stationID @ >< i + &4 hex u0.r colon Count i cells + @ decimal . loop ; : ~data leddata high \ fixme: leddata ." __Q" \ datagram start .id+ver \ stationID + swVersion data.ls ." C--" \ datagram end leddata low ;
Since data is updated using an interrrupt service routine only, the available command loop is available to service any requests from the rs485–serial connection. If work has to be done outside the interrupt service routine, a multitasker can be used to run two tasks: one to read and process sensor data, and another one to run the command loop.
As one of my lecturers kept saying: **Afterwards** everything is obvious. However, the path to obviousness can be long and windy at times.
- Always provide a jumper to optionally disconnect the RX pin of the transceiver, IF you want to keep the existing RS232 transceiver working.
- Consider adding jumpers to disconnect the bus. This is occasionally useful.
There is probably more to be said ...
Like always this work would not have been possible without substantial help from others. Special thanks go to Matthias Trute for amForth, for providing valuable feedback and picking up suggestions; Lubos Pekny for proving, that it can be done; the members of the amforth-devel mailing list, the weekly IRC round and of the German “Forth Gesellschaft e.V.”; countless authors of documentation, code, or processes for all the countless pieces of software that comprise my workstation setup, e.g. bash, emacs and perl to name just three.
- net4801 single board computer running the collector
- Lubos Peknys mFC project using rs485 and mpc mode highly inspired this code and project
- Pavel Pisa, implementing a 9-bit microLAN