Using a board with NuttX RTOS as an RS-485 / Modbus Slave Device
Until now we saw how to connect local sensors, actuators and also some kinds of analog devices in our board, but for Industrial application it is very common to use remote devices over some bus, and RS-485 and Modbus (a protocol over physical layer of RS-485) is very common and low cost bus for this kind of application.
And a good thing about RS-485 on NuttX is because you just need an ordinary UART peripheral and a GPIO pin connected to some RS485 transceiver to use it. It means even if your microcontroller doesn't have native RS485 support, you can still get it working on NuttX. Today we will see how to do it!
Quick Links
- Part 1: Getting Started with (Apache) NuttX RTOS - Part 1
- Part 2: Getting Started with (Apache) NuttX RTOS Part 2 - Looking Inside and Creating Your Customized Image
- Part 3: Getting Started with NuttX RTOS on Three Low Cost Boards
- Part 4: Using GPIO in (Apache) NuttX RTOS
- Part 5: Using (Apache) NuttX USERLED Subsystem
- Part 6: Using (Apache) NuttX Buttons Subsystem
- Part 7: How to use I2C devices in (Apache) NuttX: Scanning for Devices
- Part 8: How to use I2C devices in (Apache) NuttX: Adding support for an I2C device in your board
- Part 9: How to use SPI devices in NuttX RTOS
- Part 10: How to use analog input (ADC) on NuttX RTOS
- Part 11: Using a board with NuttX RTOS as an RS-485 / Modbus Slave Device
First lets to get a very quick introduction to RS-485. The standard was defined in 1983 as TIA-485(-A) or EIA-485, it was a joint between Telecommunications Industry Association (TIA) and Electronic Industries Alliance (EIA). They defined only the physical layer, not the protocol to use over it (CAN Bus introduced at late 80s also follows the same approach). There are at least two wired used by RS-485, they are the differential signals A and B (TIA also requires a third common wire, aka GND).
The same way to get RS-232 signal from UART we need to use a transceiver, for RS-485 we also need a special transceiver (i.e. MAX485) to use it. The image below shows the differential signals (A and B) used for RS-485 (source: Wikipedia)
Something interesting to note is that signal A is similar ("equivalent") to the RS-232 signal, this way it is called non-inverting signal. The B signal is the inverted (complement) of A. The differential signal makes RS-485 at some point resistant to external EMI and it allows long distance communication (up to 4000 ft or 1200m) and data rates up to 10-Mbps. Since the RS-485 doesn't have native collision detection/avoidance, some care is needed to avoid two or more devices transmitting at same time.
How does NuttX implement support to RS-485?
Basically the logic to support RS-485 is straight-forward: just before the UART peripheral start to transmit the driver will enable (write to) the GPIO DIR pin connected to the DE/RE pin of the transceiver. Note that the pins DE and /RE of the transceiver are tied together. This way when DE is enabled the transceiver allow the UART to transmit and avoid receiving data, since /RE is de-asserted.
When the UART peripheral ends transmitting a Transmit Complete (TC) or equivalent interrupt will be generated, in this moment the GPIO DIR pin is disabled, then the transceiver will be allowed to receive data from the RS-485 bus. Unfortunate the RP2040 doesn't have TC interrupt.
It happened because Raspberry Pi Foundation opted to use the PrimeCell UART PL011 IP from ARM, that in my personal option was a bad decision, compared to other vendors this IP has many limitations. So to implement a RS-485 using this UART we need to setup the FIFO depth to 1 (disabling FIFO) and do polling in the FIFO busy status to know if the transmission finished. This solution is not elegant for a kernel driver and currently is not implemented in the RP2040 UART driver (I plan to add support to it anyway).
So we will use the STM32F4Discovery to get RS-485 working with Modbus protocol. For curious readers, the RS485 support is implemented at: nuttx/arch/arm/src/stm32/stm32_serial.c .
What do we need to make our board become a Modbus Slave device and to test it?
We need at least two things (besides our board with NuttX): a RS-485 Transceiver and a USB/RS-485 adapter.
We will use a RS485 transceiver connect to the USART1. I opted to use a low cost module powered by MAX485 because it is low cost and very easy to find on eBay and Aliexpress:
The USB/RS-485 adapter also is easy to find on eBay or Aliexpress. I used the simplest model without a plastic cover:
Normally almost all models works fine on Linux, Windows (and possibly on MacOS and BSDs too).
Finally you will need a tool to read/write to USB/RS-485 adapter to get data and send requests to your Modbus Slave Device (our NuttX board in this case). There is a tool called mbpoll tool that you can install using this command:
$ sudo apt install mbpoll
If you use Windows or MacOS, you need to find an equivalent tool (TODO).
Connecting a transceiver to the STM32F4Discovery board
This table shows how to connect the USB/RS-485 transceiver to STM32F4Discovery board:
STM32F4Discovery Pin Name |
RS-485 Transceiver Pin Name |
GND | GND |
5V | VCC |
PB6 | DI |
PB7 | RO |
PA15 | DE+RE (connected to both) |
Note: the pin PA15 needs to be connected to DE and RE, then it is easy to solder the pads of the header pins together and just use a wire.
After that you can use a 1 meter (around 3 ft) wire to connect the label A from USB/RS-485 adapter connector to the label A of the RS-485 Transceiver module and another 1 meter wire the connect label B in the same way. For long distances it is recommended to also connect the GND from transceiver and adapter board.
Configuring the NuttX RTOS to use Modbus Slave
Fortunately there is already a modbus slave example for STM32F4Discovery that I added to NuttX mainline some years ago, you can configure this board profile this way:
$ cd nuttxspace/nuttx $ make distclean $ ./tools/configure.sh stm32f4discovery:modbus_slave
Although everything is already configured, I will show here the most important point that this board profile enables in the menuconfig.
These are the configuration
System Type ---> STM32 Peripheral Support ---> [*] USART1 [*] USART2
Remember that USART2 is used as serial console (connected over a USB/Serial on pins PA2 and PA3, see https://embeddedrelated.com/showarticle/1610.php for more info). Then we are enabling USART1 here to be used as RS485 interface, we need to define that USART1 will be used as RS-485 interface:
System Type ---> U[S]ART Configuration ---> [*] RS-485 on USART1 (1) USART1 RS-485 DIR pin polarity
And we need to defined the default baud-rate for our RS-485, normally RS-485 devices use 38400 8E1, but it is not a rules (always check your device manual)
Device Drivers ---> [*] Serial Driver Support --- USART1 Configuration ---> (256) Receive buffer size (256) Transmit buffer size (38400) BAUD rate (8) Character size (2) Parity setting (0) Uses 2 stop bits
Finally we need to enable the FreeModBus library:
Application Configuration ---> FreeModBus ---> [*] Modbus support using FreeModbus (16) Maximum number of Modbus functions [*] Modbus slave support via FreeModBus [*] Modbus ASCII support [*] Modbus RTU support [ ] Modbus TCP support (1) Character timeout (0) Timeout to wait before sending (32) Size of Slave ID report buffer [*] Report Slave ID function [*] Read Input Registers function [*] Read Holding Registers function [*] Write Single Register function [*] Write Multiple registers function [*] Read Coils function [*] Write Coils function [*] Write Multiple Coils function [*] Read Discrete Inputs function [*] Read/Write Multiple Registers function [ ] Modbus Master support via FreeModBus
Now that you know what this board profile does, you can leave the menuconfig and compile NuttX:
$ make -j
Then the compilation finishes:
Create version.h LN: platform/board to /home/alan/nuttxspace/apps/platform/dummy CPP: nxfonts_convert.c-> nxfonts_convert_24bpp.i Register: hello Register: nsh Register: modbus Register: sh ... LD: nuttx Memory region Used Size Region Size %age Used flash: 93540B 1 MB 8.92 sram: 7632 B 112 KB 6.65 CP: nuttx.hex CP: nuttx.bin
You can flash the firmware in the STM32F4Discovery using the OpenOCD command:
$ sudo openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c init -c "reset halt" -c "flash write_image erase nuttx.bin 0x08000000" [sudo] password for alan: Open On-Chip Debugger 0.12.0 Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Info : auto-selecting first available session transport "hla_swd". To override use 'transport select '. Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD Info : clock speed 2000 kHz Info : STLINK V2J14S0 (API v2) VID:PID 0483:3748 Info : Target voltage: 3.206815 Warn : target stm32f4x.cpu examination failed Info : starting gdb server for stm32f4x.cpu on 3333 Info : Listening on port 3333 for gdb connections Info : [stm32f4x.cpu] Cortex-M4 r0p1 processor detected Info : [stm32f4x.cpu] target has 6 breakpoints, 4 watchpoints [stm32f4x.cpu] halted due to debug-request, current mode: Thread xPSR: 0x01000000 pc: 0x08000188 msp: 0x20001700 Info : device id = 0x10036413 Info : flash size = 1024 KiB auto erase enabled wrote 131072 bytes from file nuttx.bin in 5.444160s (23.511 KiB/s) Info : Listening on port 6666 for tcl connections Info : Listening on port 4444 for telnet connections ^Cshutdown command invoked
Testing NuttX as a Modbus Slave on STM32F4Discovery
It is important to know that the USB/Serial used as serial console to access the NuttX Shell (NSH) needs to be plugged before plugging the USB/RS-485 adapter, because both will create a /dev/ttyUSB* interface. So our USB/Serial will be /dev/ttyUSB0 and our USB/RS-485 will be /dev/ttyUSB1.
So, let to open minicom (that is using the /dev/ttyUSB0 at 115200 8N1) to access the NuttShell:
$ minicom
Initially nothing happened, then press the Reset bottom of STM32F4Discovery and you will see:
NuttShell (NSH) NuttX-12.7.0 nsh> nsh> ? help usage: help [-v] [] . cmp false mkrd rmdir unset [ dirname fdinfo mh set uptime ? dd free mount sleep usleep alias df help mv source watch unalias dmesg hexdump mw test xd basename echo kill pidof time wait break env pkill printf true cat exec ls ps truncate cd exit mb pwd uname cp expr mkdir rm umount Builtin Apps: hello modbus nsh sh nsh>
Note that we got a modbus command as "Builtin Apps", we need to run start the modbus example this way:
nsh> modbus -e
It will keep waiting for commands:
Now open other Linux terminal and execute the command mbpoll to read data from our Modbus Slave device:
$ sudo mbpoll -a 10 -b 38400 -t 3 -r 1000 -c 1 /dev/ttyUSB1 -R
These random numbers (7853, 16382, 20045, etc) are value generated by our modbus example (look the source code at nuttxspace/apps/examples/modbus/modbus_main.c).That is it! I hope you have enjoyed this demo and now you can create your application to send data over modbus to your computer. Merry Christmas and Happy New Year!!!
- Comments
- Write a Comment Select to add a comment
To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.
Please login (on the right) if you already have an account on this platform.
Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: