EmbeddedRelated.com
Blogs
Imagine Conference

Using a board with NuttX RTOS as an RS-485 / Modbus Slave Device

Alan C AssisDecember 22, 2024

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

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).

This article is available in PDF format for easy printing

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!!!



 



Imagine Conference

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: