Learning a new programming language is one of the most rewarding and satisfying things you can do! In this article, I'll describe how I (a regular Arduino aficionado) fell in love with the Ada programming language while developing a portable MODBUS-RTU Master that can communicate with other Modbus-enabled industrial equipment (like VFDs, flow rate sensors, etc.)
If you read till the end, you'll notice that there are many improvements that can be made to the Ada Modbus driver as well as the device. If you're up to the challenge, help me develop this into a fully featured open source Modbus analyzer!
What is Ada?Ada is a programming language designed with reliability and efficiency in mind. To put it simply, the structure of the Ada programming language minimizes the risk of your code failing at some point in time by forcing you to write code in a very concise manner. Due to its reliability, Ada is seeing significant usage worldwide in high-integrity / safety-critical / high-security domains including commercial and military aircraft avionics, air traffic control etc. You can read more about Ada and its applications here.
In order for you to start working with Ada, you'll have to download the GNAT Community Edition from Adacore.
https://www.adacore.com/download
After installing BOTH.exe files, open up GPS.exe (GNAT Programming Studio), and get familiar with the application.
Learning AdaBefore we move onto hardware, it's imperative that you gain a basic understanding about Ada. I referred to two main sources when starting out.
- AdaCore U YouTube Playlist
- https://learn.adacore.com/ - This is an interactive platform, and you can run your Ada programs directly on the browser while you learn.
At first you'll feel that the Ada compiler is a bit strict, but you will start to appreciate it as you keep coding.
HardwareAda supports a few commonly available development boards such as the BBC Microbit, OpenMV2 and STM32 Discovery boards. This project was done with a STM32F407VGT6. If you're starting out, I highly recommend purchasing the STM32F4 DISCOVERY Kit, which comes with an onboard ST-Link V2 for uploading and debugging code.
Since I had a ST-Link V2 lying around, I decided to take a cheaper route with a development board I found on Ebay.
If you're on a tight budget, you could get this and a STLink-V2 although the official board is absolutely worth it for the price. If you do purchase the cheaper board, make sure you pull BOOT0 to ground when uploading code using ST-Link. It took me a few hours to figure out that booth pins were not connected to ground.
For the remaining part of this project, I'll assume that you purchased the cheaper board, so that a wider range of people can follow this demo. (All the instructions remains the same no matter what board you have)
LED DemoGPS comes with a demo application for the STM32F4. You can create a project with this template from the 'Create new project' window.
Since we're using ST-Link, you'll have to change the upload method from Edit > Project Properties > Embedded.
Once that's done, you can flash the program to your board. You won't see any lights blinking because the cheap board does not have any! Connect some LEDs to pins PD12-PD15, and you can see them blinking in a pattern.
If you observe the main.adb file of the project, you'll notice that the loop of the program does not contain any code.
procedure Main is
pragma Priority (System.Priority'First);
begin
loop
null;
end loop;
end Main;
If you dive deeper into the code, you'll notice that the blinking part happens inside the driver.adb file. That's right! Ada has tasking support built right inside the language. For embedded applications, tasking support is provided by a profile called Ravenscar. You might have come across this name if you checked the led_demo.gpr.
for Runtime ("Ada") use "ravenscar-sfp-stm32f4";
To learn more about Tasking in Ada, visit https://blog.adacore.com/theres-a-mini-rtos-in-my-language. The author explains everything far better than I ever could, and with examples!
Ada Drivers LibraryThis is another important set of tools you'll need to start working with Ada. This Library contains drivers and example codes for everything from initializing a GPIO pin to interfacing with common devices like OLED displays (like the SSD1306 used in this project), accelerometers (MPU9250) etc.
You can download the Ada Drivers Library from Github.
https://github.com/AdaCore/Ada_Drivers_Library
After downloading, you'll have to add the library to your project. We'll use the led_demo as the base for this tutorial. Go to Edit > Project Properties > Dependencies and press the + sign to add files. We'll need to add
- stm32f407_discovery_full.gpr
(found in Ada_Driver_Library/ boards /stm32f407_discovery ) and
- common.gpr
(found in Ada_Drivers_Library-master \examples \shared \common \common.gpr )
After adding these dependencies, make sure that your <project_name>.gpr file starts like this. Note : The path will change depending on where you placed the Ada Drivers Library.
with "..\Ada_Drivers_Library-master\boards\stm32f407_discovery\stm32f407_discovery_full.gpr";
project Adambus extends "..\Ada_Drivers_Library-master\examples\shared\common\common.gpr" is
After that, you're all set! Configuring peripherals and interfacing with devices over I2C/ SPI becomes really easy with the provided drivers. However, I hope that as the community grows, people will add more and more example codes to the Ada Driver Library! Learning by example is very effective (just look at the Arduino platform. Because of its large community, there are tons of libraries and example codes. More examples means its easier for newcomers to grasp concepts of the language quickly).
From this point onward, we will focus on the Ada Modbus Analyzer. I will explain how to initialize each major component (Modbus, USART, I2C, interrupts), so that you can reuse the same code when building your own projects.
Ada Modbus AnalyzerThe main objective was to create a handheld device that could easily be carried around. ( in contrast to a bulky laptop and a RS485 - USB adapter ). Such a device could be used in the field to interface with Modbus RTU enabled slaves, such as Variable Frequency Drives, Power Meters, and Flow rate sensors for troubleshooting and minor data monitoring applications.
Note : MODBUS is a serial communication protocol developed by Modicon. It is an open protocol( as opposed to some closed industrial communication protocols like MPI), which means that anyone can use it in their devices without having to pay royalties. The complete MODBUS manual is available in their website -> http://modbus.org/docs/PI_MBUS_300.pdf
Example for a Real life scenario #1 : To observe the Steam Flow of an Industrial Steamer Machine at Startup. (Pictured below are display units of the Forbes-Marshal steam flow rate Sensors fixed to said machine).
Example for a real life scenario #2: To easily view the acceleration parameters set in a Schnieder Electric Altivar 12 Variable Frequency Drive.
Too keep things simple, I first narrowed down the operation of the device to the following key stages.
In order to get the Slave Address and the Holding register address from the user, I used four push buttons.
I also decided to use USART1 to send and receive information to/from the slave device. A 3.3V RS485 - TTL converter was used to transmit and receive over RS485.
Due to it's low cost and ease of use, I selected a SSD1306 128 x 64 OLED I2C display. The Ada Driver Library also comes with a driver for the SSD1306, so I didn't have to make everything from scratch.
We'll now look at the core packages which were written/used for this project.
SerialThe Ada Drivers Library contains an example code for this. I used the polling method since I currently don't have anything planned for the device to do while sending and receiving data. (In the future I hope to implement data logging capabilities to this project, and I'm hoping to switch to interrupts then.)
In your project, create two files to hold your serial related functions. I used serial.adb and serial.ads in mine. (By now I hope you know why we use adb and ads files)
In your serial.adb, import the necessary packages and declare the variables, procedures and functions we leave visible. (Note : You'll have to successfully add the Ada Drivers Library to your project to import packages such as STM32.GPIO and STM32.USARTs )
with HAL; use HAL;
with STM32.GPIO; use STM32.GPIO;
with STM32.USARTs; use STM32.USARTs;
with STM32.Device; use STM32.Device;
package Serial is
--Pins
TX_Pin : constant GPIO_Point := PA9;
RX_Pin : constant GPIO_Point := PA10;
--UART Functions
procedure Initialize_UART_GPIO;
procedure Initialize;
procedure Await_Send_Ready (This : USART) with Inline;
procedure Await_Receive_Ready(This : USART) with Inline;
procedure Put_Blocking (This : in out USART; Data : UInt16);
function Receive_Blocking (This : in out USART) return UInt8;
end Serial;
The default pins for USART1 is PA9 and PA10 for the STM32F407.
Refer to my serial.adb file (Github Link to the project is at the end of this article) Although I won't explain every line of code, I'd like to draw your attention to these two procedures.
procedure Initialize_UART_GPIO is
begin
Enable_Clock (USART_1);
Enable_Clock (RX_Pin & TX_Pin);
Configure_IO
(RX_Pin & TX_Pin,
(Mode => Mode_AF,
AF => GPIO_AF_USART1_7,
Resistors => Pull_Up,
AF_Speed => Speed_50MHz,
AF_Output_Type => Push_Pull));
end Initialize_UART_GPIO;
----------------
-- Initialize --
----------------
procedure Initialize is
begin
Initialize_UART_GPIO;
Disable (USART_1);
Set_Baud_Rate (USART_1, 9600);
Set_Mode (USART_1, Tx_Rx_Mode);
Set_Stop_Bits (USART_1, Stopbits_1);
Set_Word_Length (USART_1, Word_Length_8);
Set_Parity (USART_1, No_Parity);
Set_Flow_Control (USART_1, No_Flow_Control);
Enable (USART_1);
end Initialize;
This initialization (Serial.Initialize in my case) must be done prior to using USART1. The pins are set to their alternate functions, pulled up, and the USART1 port is configured with the baud rate, parity etc. A similar process will have to be followed when utilizing others such as I2C, I2S and SPI.
MbusThe modbus package is the cornerstone of this project. In modbus.ads you'll see the constants, buffers and functions used to implement the modbus protocol. Most of the variables are unsigned 8 or unsigned 16, which means that you require the HAL package to create them. The following functions can be used as of now, and I'm hoping to implement the MODBUS Write functions in the near future.
procedure Mbus_begin(SlaveAddr: UInt8);
-- Sets the slave address and clears buffer indexes
function readHoldingRegisters(ReadAddr,ReadQty_temp : UInt16) return UInt8;
--read holding registers. This is the most commonly used one for data acquisition.
function readCoils(ReadAddr,ReadQty_temp : UInt16) return UInt8;
--read coils
function readDiscreteInputs(ReadAddr,ReadQty_temp : UInt16) return UInt8;
--read Discrete Inputs
function readInputRegisters(ReadAddr,ReadQty_temp : UInt16) return UInt8;
--read input registers
All the magic of the modbus communication happens inside the Mbus_Transaction function. When we need to request data from a slave address, the modbus data unit must be assembled in the way specified by Modicon. Since the some components of the units are 16 bits long (such as the Register Address) these must be split into two 8 bit parts. This is accomplished by the lowByte and highByte procedures.
--Get High Byte
function highByte(Val : UInt16) return UInt8 is
begin
return UInt8(Shift_Right(Val,8));
end highbyte;
--Get Low Byte
function lowByte(Val: UInt16) return UInt8 is
begin
return UInt8(Val and 16#FF#);
end lowbyte;
The Modbus_Transaction function assembles the data unit into the ModbusADU array (of type UInt8) in the following way.
First element of the data unit is the Slave address.
ModbusADU(ModbusADUSize) := MBSlave;
ModbusADUSize := ModbusADUSize + 1;
Second unit is the function code (eg : Read Holding Registers has the function code of 16#03# (16#__# denotes a hexadecimal number)).
MBFunction is passed to the Modbus_Transaction function by the user by calling functions such as readHoldingRegisters, readDiscreteInputs etc. (mbus.ads contains all function codes, currently only the reading functions are implemented)
ModbusADU(ModbusADUSize) := MBFunction;
ModbusADUSize := ModbusADUSize + 1;
Next step is to add the high byte of the register address we need to request data from.
Eg : For the Altivar12, the register that holds the acceleration time is 16#2329# = 09001 (decimal).
Then the low byte of the address is added to the data unit. We can request a number of addresses STARTING from the address we specified previously. The high byte and low byte of this value is added to the data unit. Currently in my Modbus Analyzer, this value is always 1, but with your help, it can be improved to display values of multiple addresses!
if MBFunction = MBReadCoils or MBFunction = MBReadHoldingRegisters or MBFunction = MBReadDiscreteInputs or MBFunction = MBReadInputRegisters then
ModbusADU(ModbusADUSize) := highByte(ReadAddress);
ModbusADUSize := ModbusADUSize + 1;
ModbusADU(ModbusADUSize) := lowByte(ReadAddress);
ModbusADUSize := ModbusADUSize + 1;
ModbusADU(ModbusADUSize) := highByte(ReadQty);
ModbusADUSize := ModbusADUSize + 1;
ModbusADU(ModbusADUSize) := lowByte(ReadQty);
ModbusADUSize := ModbusADUSize + 1;
end if;
Now that the data unit is assembled, we have to generate a CRC for it. This is done using the crc_update function shown below.
--Update CRC
function crc16_update(CRC: in out UInt16; a : UInt8) return UInt16 is
begin
CRC := CRC xor UInt16(a);
for i in UInt8 range 0..7 loop
if UInt8(CRC and 16#0001#) = 1 then
CRC := Shift_Right(CRC,1) xor 16#A001#;
else
CRC := Shift_Right(CRC,1);
end if;
end loop;
return CRC;
end crc16_update;
In our Modbus_Transaction function, we generate the CRC for the data unit in the following way.
CRC := 16#FFFF#;
--Calculate CRC
for i in UInt8 range 0..ModbusADUSize-1 loop
CRC := crc16_update(CRC, ModbusADU(i));
end loop;
ModbusADU(ModbusADUSize) := lowByte(CRC);
ModbusADUSize := ModbusADUSize + 1;
ModbusADU(ModbusADUSize) := highByte(CRC);
ModbusADUSize := ModbusADUSize + 1;
The CRC is then added at the end of the data unit. The slave and the master both utilizes this to check for any errors during data transmission. Read more about CRC here.
Afterwards, the data unit is sent by utilizing the serial package we made earlier. (Put_Blocking). As soon as we send the data, we wait for a response from the slave. If there is no reply, a timeout occurs.
Since posting too much code will clutter up the article, you can see how the received data unit is processed in mbus.adb.
My_I2CUnlike the serial example, I had to dig deeper into the Ada Drivers library to figure out how to initialize I2C. (Special mention to Blast_545 for pointing me in the right direction in the Discussion forum!) You'll see all the required functions in the my_i2c package. The initialization function can be found in my_i2c.adb. Also note that you have to import the STM32.Setup package for this to work.
function Initialize return Boolean is
begin
STM32.Setup.Setup_I2C_Master (Port => I2C_1,
SDA => Screen_I2C_SDA,
SCL => Screen_I2C_SCL,
SDA_AF => GPIO_AF_I2C1_4,
SCL_AF => GPIO_AF_I2C1_4,
Clock_Speed => 100_000);
return True;
end Initialize;
Screen_I2C_SDA and SCL are GPIO_Points declared in my_i2c.ads. For I2C_1 port, these pins are PB7 and PB6.
ScreenThis handles the SSD1306 display using the helper functions from LCD_Std_Out package available in the Ada Driver Library. When I first started out, I edited the SSD1306.ads file in /components/screen/ssd1306. I replaced the provided address, 0x78, with what I thought was the correct address, 0x3C (used for Arduino). I couldn't get the display to work, and it took me a while to find out that the Ada I2C driver is written a bit differently than in Arduino. You'll notice that,
0x78 = 0b1111000
0x3C = 0b111100,
I suggest you leave the declared register values alone even if they're different than the ones you're familiar with in Arduino.
screen.adb contains all the functions in LCD_Std_Out.adb, but they're modified to work with the single color OLED display. However, the only functions you need to know about are,
procedure Clear_Screen;
Pretty self explanatory. This will clear the screen.
procedure Put_Line (Msg : String);
Wraps around to the next line if necessary. Always calls procedure New_Line automatically after printing the string.
procedure Put (Msg : String);
procedure Put (Msg : Character);
Puts a string or character onto the screen with generating a new line at the end.
procedure New_Line;
Generates a new line.
procedure Put (X, Y : Natural; Msg : String);
Prints the string, starting at the location specified by X and Y.
ButtonFour pins were configured for external interrupts. You can see the initialization functions in button.adb. You can easily set up pins using the helper functions available in the STM32.GPIO package.
The following lines of code sets up a GPIO_Point SW1 as an input with interrupts enabled, pulled up and trigger set to falling edge.
Enable_Clock(SW1);
SW1.Configure_IO
((Mode => Mode_In,
Resistors => Pull_Up));
SW1.Configure_Trigger (Edge);
'Edge' is specified in button.ads.
Edge : constant STM32.EXTI.External_Triggers :=Interrupt_Falling_Edge;
You need the STM32.EXT1 package to declare this.
The LED package is similar to the Button package, with 1 pin being configured as an output to flash an LED routinely. This happens as a separate task. The code in led.ads, led.adb, driver.abs and driver.adb is pretty self explanatory. (I left this part in the code to test out Ada's tasking capabilities.
Main LoopNow that we have everything setup and ready to go, all that's left is to create a simple user interface for the Modbus Analyzer. This is done within the main program loop as a three stage process, where the user first enters the Slave Address, and then the Register Address. The value obtained from the slave is then displayed until the user presses a button to exit, where the process starts over. You can see how it works in main.adb.
Note : Currently parameters such as the baud rate cannot be set using the user interface, but I hope to implement that functionality in the future!
PCB and EnclosureAny good embedded project needs a nice printed circuit board. I designed the PCB for the Modbus Analyzer using Eagle. If you're one of the people who purchased the same STM32F407 board I did, you can also use the EAGLE library I made for this board in your future projects! The library is available in the Github Repository under Eagle/lbr.
(If you're interested in creating custom parts, refer to this video on YouTube).
Since most of the PCB fabrication houses were closed due to Chinese New Year, I had to make this PCB the good old way..Etching! (If you've never etched a PCB, I suggest you try it at least once)
And here's what it looks like after soldering all the components. I also added a 3D printed enclosure to the bottom side prior to testing the board.
After making sure that there were no shorts (There is ALWAYS one), I powered up the device and tested it with an Altivar 12 VFD.
You'll notice that the PCB has a terminal block for MODBUS RTU devices without an RJ45 port. (I added the RJ45 port specifically for the Altivar 12)
Time to start testing!
Special Note : You can find the Modbus communication parameters for your slave device in its manual. In this case I referred to the Altivar 12 communication parameters document found in Schneider Electric's website.
Special Note 2 : This modbus analyzer is configured for a baud rate of 9600. If your device does not support this/ or you do not have access to change it (usually 9600 is commonly supported ) you'll have to change this value in serial.adb. **Configurable baud rates will be added to the user interface really soon!
All that was left to do was to design the top part of the enclosure (I have to agree, designing using SolidWorks is not one of my strong points)
And here's a picture of everything prior to assembly.
Learning a language becomes much more interesting when you get to apply it in real life! Ada is a great programming language to learn if you are planning to expand your scope beyond Arduino. There are plenty of online lectures, blogs and forums to help you out, and the Ada Driver Library allows you to add various sensors to your projects.
As more and more people start learning Ada and making projects with it, it'll be easier for everyone, since the Ada community will become stronger and there will be tons sample codes and libraries!
If you happen to notice any errors in my code, or any bad practices, please let me know, since I am still relatively new to Ada!
This project is completely open source, and with your help, it could turn into a really great tool that engineers can use for data logging, troubleshooting and configuring MODBUS enabled devices.
Comments