Monday, 17 November 2008

BCD to Binary conversion

I needed to convert some numbers received from a PLC from their original packed BCD format to binary (hex) format. The numbers to convert are split into two 16-bit halves, and need to be converted into one 32-bit number.

(Packed BCD values contain two digits for every byte.)

There is a method that exists that will convert numbers from BCD into a true hex number, by using string formatting, but this is relatively expensive in terms of performance and I required the best performance possible.

So, I came up with this function:

   1:  /// <summary>
   2:  /// Convert two PLC words in BCD format (forming 8 digit number) into single binary integer.
   3:  /// e.g. If Lower = 0x5678 and Upper = 0x1234, then Return is 12345678 decimal, or 0xbc614e.
   4:  /// </summary>
   5:  /// <param name="lower">Least significant 16 bits.</param>
   6:  /// <param name="upper">Most significant 16 bits.</param>
   7:  /// <returns>32 bit unsigned integer.</returns>
   8:  /// <remarks>If the parameters supplied are invalid, returns zero.</remarks>
   9:  private uint BCD2ToBin(uint lower, uint upper)
  10:  {
  11:      uint binVal = 0;
  12:   
  13:      if ((lower | upper) != 0)
  14:      {
  15:          int shift = 0;
  16:          uint multiplier = 1;
  17:          uint bcdVal = (upper << 16) | lower;
  18:   
  19:          for (int i = 0; i < 8; i++)
  20:          {
  21:              uint digit = (bcdVal >> shift) & 0xf;
  22:   
  23:              if (digit > 9)
  24:              {
  25:                  binVal = 0;
  26:                  break;
  27:              }
  28:              else
  29:              {
  30:                  binVal += digit * multiplier;
  31:                  shift += 4;
  32:                  multiplier *= 10;
  33:              }
  34:          }
  35:      }
  36:   
  37:      return binVal;
  38:  }


The function takes in two unsigned itegers that hold each of the 16-bit weighted BCD values, and returns a 32-bit unsigned integer.

It is important to note that the return is an unsigned integer, as this allows BCD values up to 0x99999999 to be converted correctly.

I have also taken the approach of returning zero if any of the values supplied are incorrect, i.e. contain an invalid digit. (BCD values can only contain digits ranging between 0 and 9, and not A to F.)

Of course, it may be desirable to return a different value, or to throw an exception if the parameters supplied are invalid, but for my purposes, returning a zero was sufficient.

This method proved to be around four to five times quicker than by using the string format method.

Friday, 24 October 2008

Using RowPrePaint event of a DataGridView control

To draw a row in a DataGridView control in a different style depending on a certain condition, you can use the RowPrePaint event to interrupt the painting of the rows in the grid.

You then have a choice of which parts of the row to be painted automatically and which parts to paint yourself via the PaintParts event property.

In the code below, I paint a row with a custom brush, depending on the value of a column that is named 'Error'.

   1:  /// <summary>
   2:  /// Handles the drawing of each DataGridView row depending on set log error state.
   3:  /// </summary>
   4:  /// <param name="sender"></param>
   5:  /// <param name="e"></param>
   6:  private void setLogDataGridView_RowPrePaint(object sender, DataGridViewRowPrePaintEventArgs e)
   7:  {
   8:      // Paint all parts of row except SelectionBackground and the Focus box.
   9:      e.PaintParts = DataGridViewPaintParts.All & ~(DataGridViewPaintParts.SelectionBackground | DataGridViewPaintParts.Focus);
  10:   
  11:      // Get the value of the error column of the current row
  12:      string val = this.setLogDataGridView.Rows[e.RowIndex].Cells["Error"].Value.ToString();
  13:   
  14:      if (val != "0")
  15:      {
  16:           // Get rectangle of area to paint with custom brush.
  17:            Rectangle rowBounds = new Rectangle(
  18:             this.setLogDataGridView.RowHeadersWidth, e.RowBounds.Top,
  19:             this.setLogDataGridView.Columns.GetColumnsWidth(
  20:             DataGridViewElementStates.Visible) -
  21:             this.setLogDataGridView.HorizontalScrollingOffset + 1,
  22:             e.RowBounds.Height);
  23:   
  24:      // Disable normal painting of backgrounds.
  25:      e.PaintParts &= ~(DataGridViewPaintParts.Background | DataGridViewPaintParts.ContentBackground);
  26:   
  27:      e.PaintHeader(false);
  28:   
  29:      // Paint the custom background.
  30:      using (Brush lgb = new LinearGradientBrush(rowBounds, Color.Red,
  31:             Color.DarkRed, LinearGradientMode.Vertical))
  32:      {
  33:          e.Graphics.FillRectangle(lgb, rowBounds);
  34:      }
  35:   
  36:      // Set the text colour.
  37:      this.setLogDataGridView.Rows[e.RowIndex].DefaultCellStyle.ForeColor = Color.Yellow;
  38:      this.setLogDataGridView.Rows[e.RowIndex].DefaultCellStyle.SelectionForeColor =      Color.Yellow;
  39:      }
  40:  }

Create a User business object

To create a business object that represents a 'User' of my Datalog application, I have used the code shown below, and it works well for my purposes. I decided to make it a Struct instead of a Class as it is a small and lightweight object. If any readers think that it should preferably be a Class, then please leave a comment. I'm still learning this stuff, so any tips welcome!

   1:  using System;
   2:   
   3:  namespace KAS.Datalog
   4:  {
   5:      /// <summary>
   6:      /// Represents a user of the application.
   7:      /// </summary>
   8:      public struct User
   9:      {
  10:          #region Fields
  11:   
  12:          private string _strName;
  13:          private UserLevel _userLevel;
  14:   
  15:          #endregion
  16:   
  17:          #region Properties
  18:   
  19:          /// <summary>
  20:          /// Gets or sets the user name.
  21:          /// </summary>
  22:          public string Name
  23:          {
  24:              get { return this._strName; }
  25:              set { this._strName = value; }
  26:          }
  27:   
  28:          /// <summary>
  29:          /// Gets or sets the user level.
  30:          /// </summary>
  31:          public UserLevel Level
  32:          {
  33:              get { return this._userLevel; }
  34:              set { this._userLevel = value; }
  35:          }
  36:   
  37:          #endregion
  38:   
  39:          #region Constructor
  40:   
  41:          /// <summary>
  42:          /// Constructor
  43:          /// </summary>
  44:          /// <param name="name">Name of the user</param>
  45:          /// <param name="level">Secutirty level</param>
  46:          public User(string name, UserLevel level)
  47:          {
  48:              if (name != "") { this._strName = name; }
  49:              else { this._strName = "Unknown"; }
  50:   
  51:              this._userLevel = level;
  52:          }
  53:   
  54:          #endregion
  55:   
  56:          #region Overridden methods
  57:   
  58:          public override string ToString()
  59:          {
  60:              return this._strName + "," + this._userLevel.ToString();
  61:          }
  62:   
  63:          #endregion
  64:      }
  65:  }

Friday, 25 July 2008

Toshiba Inverters

I am currently using inverters from Toshiba, namely the VF-nC1S and VF-S11 models.

Petty normal inverters when all is said and done, but with a couple of big advantages for the applications I am working with:

1. 110V single phase input models.
This is a big plus, since they still provide 220V three phase motor control and it allows use on U.S supplies (and others) that use 110V 1-ph. It also means that all machine units can be built and wired with a standard motor, with only the inverter needing to be changed depending on the shipping location. No 110V to 220V step-up transformers!

2. Three speed control inputs.
Three inputs as standard on the VF-S11, and by configuring a multi-use input on the VF-nC1S allows for eight distinct binary selected frequency selections. Sixteen can be selected by configuring a fourth input.



The VF-S11 models are slightly higher spec in that they have more functions and control I/O at your disposal.

A drawback of the 110V models is the lack of a built-in EMC filter, so an external one is required.

Also, the particular VF-nC1S model I am currently using - the 0.4kW model - has a relatively high earth leakage current. This is stated as 11mA in the specs, and measured at around 6-10mA depending on load. This can prove a problem if multiple units are powered from the same supply due to nuisance ELCB tripping, and the requiremnet to use a much more stringent earthing policy depending on the arrangement of units.

Tuesday, 22 July 2008

Omron PLC Dummy I/O

Using dummy IO modules on an Omron PLC with a 'virtual backplane' (i.e. a CJ1M for example) is a good way to keep addresses used for varying projects the same, and adding extra I/O when needed for extra functions, without upsetting the existing addressing structure.

For example, suppose you have a machine process that requires three 16 way input cards, and three 16 way output cards. These could of course occupy address space 000, 001, 002, 003, 004 and 005 without problem. But then suppose their is a need for a similar process with some limited functionality that only required two input and output modules. Once again, these could occupy addresses 000, 001, 002 and 003, but these now conflict with the earlier allocation and could cause confusion when wiring and producing documentation, because where 'Solenoid 1' connected to output 003.00 previously, it now connects to output 002.00.

A solution to this is to insert some 'dummy' I/O modules into the I/O table to act as placeholders and to allow the addressing to remain consistent between the two processes. ('Dummy' I/O modules are not actual physical modules, but rather they are virtual.)

The picture below show an I/O table with some dummy modules inserted:



Dummy I/O modules of varying sizes can be added by selecting them from the Basic IO menu of the 'Add unit' dropdown menu.

Sunday, 20 July 2008

The Moeller Wiring Manual

The Moeller Wiring Manual is an excellent resource for any electrical controls engineer.

Especially useful for the 'Specifications, Formulae, Tables' section.

Omron CJ2 PLC

Omron have announced the release of a new PLC, namely the CJ2. See this link for more info.



SYSMAC CJ2 CPU Units

1. Greatly increased program capacity and data memory capacity. The largest-capacity CPU unit, the CJ2H-CPU68-EIP, has a program capacity of 400K steps (1.6 times the capacity) and a data memory capacity of 832K words (twice the capacity of previous OMRON products).Higher system throughput speed for I/O and faster immediate refreshing for basic I/O in addition to faster basic and special instructions. (Execution time is 20 times faster than previous OMRON models, e.g., reduced from 20 µs to 1 µs for the LD instruction.)

2. A multifunctional Ethernet port compatible with EtherNet/IP is built in and can be used simultaneously for FTP communications, data links, and Support Software connections.

3. Errors reduced when modifying designs because tags are used for actual I/O and internal I/O, eliminating the need for address allocation. Also, PLC programming and PT display screens can be developed in parallel.



Of the points listed above, I find no.3 to be interesting. It sounds to me that it will make programming a PLC similar to writing a C# or Visual Basic program for example, where variables are named and used without the underlying address being important. Very interesting, and I hope that I may have the chance to give it a try.

Friday, 18 July 2008

Create a custom label control

Code for a custom label control. This creates a control that inherits from the standard System.Windows.Forms.Label control, and adds the facility to make the background a colour gradient fill.

   1:  using System;
   2:  using System.Drawing;
   3:  using System.Drawing.Drawing2D;
   4:  using System.Windows.Forms;
   5:   
   6:  namespace MyNamespace
   7:  {
   8:      public class CustomLabel : Label
   9:      {
  10:          # region Fields
  11:   
  12:          private Color _col1;
  13:          private Color _col2;
  14:          private Color _col3;
  15:          private Color _col4;
  16:          private LinearGradientMode _grad;
  17:   
  18:          # endregion
  19:   
  20:          # region Properties
  21:   
  22:          public Color Colour1
  23:          {
  24:              get { return this._col1; }
  25:              set
  26:              {
  27:                  this._col1 = value;
  28:                  this.Invalidate();
  29:              }
  30:          }
  31:   
  32:          public Color Colour2
  33:          {
  34:              get { return this._col2; }
  35:              set
  36:              {
  37:                  this._col2 = value;
  38:                  this.Invalidate();
  39:              }
  40:          }
  41:   
  42:          public LinearGradientMode Gradient
  43:          {
  44:              get { return this._grad; }
  45:              set
  46:              {
  47:                  this._grad = value;
  48:                  this.Invalidate();
  49:              }
  50:          }
  51:   
  52:          protected override Size DefaultSize
  53:          {
  54:              get { return new Size(64, 16); }
  55:          }
  56:   
  57:          # endregion
  58:   
  59:          # region Constructors
  60:   
  61:          public CustomLabel()
  62:              : base()
  63:          {
  64:              this._col1 = Color.White;
  65:              this._col2 = Color.Blue;
  66:              this._col3 = _col1;
  67:              this._col4 = _col2;
  68:              this._grad = LinearGradientMode.Vertical;
  69:              this.DoubleBuffered = true;
  70:          }
  71:   
  72:          public CustomLabel(Color c1, Color c2, LinearGradientMode grad)
  73:              : base()
  74:          {
  75:              this._col1 = c1;
  76:              this._col2 = c2;
  77:              this._col3 = _col1;
  78:              this._col4 = _col2;
  79:              this._grad = grad;
  80:              this.DoubleBuffered = true;
  81:          }
  82:   
  83:          # endregion
  84:   
  85:          # region Overrides
  86:   
  87:          protected override void OnPaintBackground(PaintEventArgs pevent)
  88:          {
  89:              //base.OnPaintBackground(pevent);
  90:   
  91:              LinearGradientBrush lgb = new LinearGradientBrush
  92:                  (this.ClientRectangle, this._col1, this._col2, this._grad);
  93:              pevent.Graphics.FillRectangle(lgb, this.ClientRectangle);
  94:              lgb.Dispose();
  95:          }
  96:   
  97:          protected override void OnEnabledChanged(EventArgs e)
  98:          {
  99:              base.OnEnabledChanged(e);
 100:   
 101:              if (this.Enabled)
 102:              {
 103:                  this._col1 = this._col3;
 104:                  this._col2 = this._col4;
 105:              }
 106:              else
 107:              {
 108:                  this._col1 = Color.LightGray;
 109:                  this._col2 = Color.White;
 110:              }
 111:   
 112:              this.Invalidate();
 113:          }
 114:   
 115:          # endregion
 116:      }
 117:  }


When this code is created, the CustomLabel control will be available to add to forms from the control toolbox. Simply add it as per any other forms control.

Notes:

Colour1 & Colour2 properties: These are the properties that specify which colours to use for the gradient fill.

Gradient: This property specifies the type of gradient to use.

Constructors: The default constructor makes the gradient coloured from white to blue, whereas the parameterized constructor colours the label with the user specified colours.

OnPaintBackground: This method is the override for the method called when the background paint event is fired. Note that the call to the base method is inhibited, (i.e. commented out!) because it is not necessary as a new background is being painted.

OnEnabledChanged: When the .Enabled property of the control is changed, an event is fired which executes this method. Notice this time that the base method is called. The colour of the control is changed to light grey through white gradient when the control is disabled to give the appearance of being 'greyed out'.

Sunday, 6 July 2008

Filling a DataGridView from a list of structs

An easy way I have found of filling a DataGridView with data, is when you configure the data source to be a list. As part of my Datalog project, I have a struct that represents a tracked machine set. Each new tracked set is added to a List<>. When the job run is complete, it is this List<> that I make the data source.

First, the code for the struct:

   1:  /// <summary>
   2:  /// Structure representing a tracked machine set.
   3:  /// </summary>
   4:  public struct TrackedSet
   5:  {
   6:      #region Fields
   7:   
   8:      private int _id;
   9:      private string _date;
  10:      private string _time;
  11:      private string _customer;
  12:      private int _sheets;
  13:      private char _insert1;
  14:      private char _insert2;
  15:      private char _insert3;
  16:      private char _insert4;
  17:      private char _insert5;
  18:      private char _insert6;
  19:      private char _insert7;
  20:      private char _insert8;
  21:      private char _insert9;
  22:      private char _insert10;
  23:      private char _sysCtrl;
  24:      private int _weight;
  25:      private string _error;
  26:   
  27:      #endregion
  28:   
  29:      #region Properties
  30:   
  31:      // The order that the properties are defined determines the order
  32:      // of columns created in the DataGridView.
  33:      // The column names will be taken from the property name, unless overwritten.
  34:   
  35:      public int ID
  36:      { get { return this._id; } }
  37:   
  38:      public string Date
  39:      { get { return this._date; } }
  40:   
  41:      public string Time
  42:      { get { return this._time; } }
  43:   
  44:      public string Customer
  45:      { get { return this._customer; } }
  46:   
  47:      public int Sheets
  48:      { get { return this._sheets; } }
  49:          
  50:      public char Insert1
  51:      { get { return this._insert1; } }
  52:   
  53:      public char Insert2
  54:      { get { return this._insert2; } }
  55:   
  56:      public char Insert3
  57:      { get { return this._insert3; } }
  58:   
  59:      public char Insert4
  60:      { get { return this._insert4; } }
  61:   
  62:      public char Insert5
  63:      { get { return this._insert5; } }
  64:   
  65:      public char Insert6
  66:      { get { return this._insert6; } }
  67:   
  68:      public char Insert7
  69:      { get { return this._insert7; } }
  70:   
  71:      public char Insert8
  72:      { get { return this._insert8; } }
  73:   
  74:      public char Insert9
  75:      { get { return this._insert9; } }
  76:   
  77:      public char Insert10
  78:      { get { return this._insert10; } }
  79:   
  80:      public char SysCtrl
  81:      { get { return this._sysCtrl; } }
  82:   
  83:      public int Weight
  84:      { get { return this._weight; } }
  85:   
  86:      public string Error
  87:      { get { return this._error; } }
  88:   
  89:      #endregion
  90:   
  91:      #region Constructor
  92:   
  93:      /// <summary>
  94:      /// TrackedSet constructor.
  95:      /// </summary>
  96:      /// <param name="id">The ID code for the tracked set</param>
  97:      /// <param name="msg">The serial link message string decoded from job data block</param>
  98:      /// <param name="created">The creation time of the tracked set</param>
  99:      /// <param name="weight">The post weight of the tracked set.</param>
 100:      /// <param name="error">The error code of the tracked set</param>
 101:      public TrackedSet(int id, DateTime created, string msg, int weight, string error)
 102:      {
 103:          // Get ID.
 104:          this._id = id;
 105:   
 106:          // Get created date.
 107:          this._date = created.ToShortDateString();
 108:   
 109:          // Get created time.
 110:          this._time = created.ToLongTimeString();
 111:   
 112:          // Get serial link message. Correct format : 12345678901,123,abcdefghij
 113:              
 114:          string[] str = msg.Split(',');
 115:   
 116:          // Get insert requests and system control characters.
 117:          char[] c = str[0].ToCharArray();
 118:   
 119:          this._insert1 = c[0];
 120:          this._insert2 = c[1];
 121:          this._insert3 = c[2];
 122:          this._insert4 = c[3];
 123:          this._insert5 = c[4];
 124:          this._insert6 = c[5];
 125:          this._insert7 = c[6];
 126:          this._insert8 = c[7];
 127:          this._insert9 = c[8];
 128:          this._insert10 = c[9];
 129:          this._sysCtrl = c[10];
 130:   
 131:          // Get sheet count.
 132:          bool result = int.TryParse(str[1], out this._sheets);
 133:   
 134:          // Get customer code.
 135:          this._customer = str[2];
 136:   
 137:          // Get post weight.
 138:          this._weight = weight;
 139:   
 140:          // Get error code.
 141:          this._error = error;
 142:      }
 143:  }


The list is defined as...

   1:  List<TrackedSet> _trackedSetList;


...and created as...

   1:  this._trackedSetList = new List<TrackedSet>();


Add new TrackedSets during a job run in this way:

   1:  // Add a new TrackedSet to the list.
   2:  this._trackedSetList.Add(new TrackedSet(this._rowCounter, 
   3:                                          DateTime.Now, 
   4:                                          this._serialLinkMessage, 
   5:                                          this._postWeightValue, 
   6:                                          this._errorCode));


Then it is simply a matter of setting the .DataSource property of the DataGridView to the list to enable it to be displayed:

   1:  this.dataGridView1.DataSource = this._trackedSetList;


This produces an output display similar to:



Click the image to see a larger view.



It is important to note thate the order that columns are created and displayed in the DataGridView is directly related to the order in which the struct properties as listed.

Friday, 4 July 2008

Omron CX-Programmer SFC Editor

While using the CX-Programmer V7.2 package from Omron, I wanted to have a play around with the SFC style of programming with a CJ1M PLC. Well, not so easy as it didn't work!

When adding a program section of the SFC type, CX-Programmer completely hung up.

After much digging, it appears that CX-Programmer (or the SFC editor alone, I'm not sure) is built upon the .NET framwork V1.1. Somehow, when the program was run, it was targetting version 2.0 of the framework, and consequently crashing.

The fix I came up with was to force the CX-P.exe program to target the correct framework version.

I should point out that the following was in no way suggested by Omron, or anyone else, and so should be taken at your own risk. OK, that's the disclaimer stuff out of the way...

Using info from this MSDN link...

...I created a file called 'CX-P.exe.config' and placed it into the CX-Programmer directory, ie. "C:\Program Files\Omron\CX-One\CX-Programmer".

Running CX-Programmer now, adding a SFC task to a CJ1M PLC for example, results in the SFC editor springing into life.

The file in question is nothing more than an XML file, the contents being:

   1:  <configuration>
   2:      <startup>
   3:          <supportedRuntime version="v1.1.4322"/>
   4:          <requiredRuntime version="v1.1.4322"/>
   5:      </startup>
   6:  </configuration>

This file forces CX-Programmer to use V1.1 of the .Net framework, and not the one it is (wrongly) using for reasons unknown. So far it appears to be working ok. Like I say, try at your own risk!

It may be that future update fixes this issue, so this is nothing more than a bit of interesting info.



I originally posted this a a thread on the MrPLC forum here.