This short tutorial describes how to configure a MODBUS server (FREEMODBUS more exactly) to run on USB instead than a standard serial port.
I suppose only a few persons will find this document useful, but why not to share nonetheless?
Note: this is not a FMB or LUFA tutorial. So I suppose you are already able to setup both.
Goal: implement a MODBUS server that can be accessed via USB
Software needed:
- FREEMODBUS (FMB) for the MODBUS server
- LUFA for the USB driver, configured as CDC device
- Part Pack from Atmel if you use the latest XMega with USB
Problem: FMB expects an ISR to signal when there is a new character in the RX buffer, It also expects another ISR that tells when the TX buffer is empty, to start sending next char.
With USB you don't have these ISRs, and you have to find a different strategy.
As FMB documentation suggests, start from the BARE implementation and change these files:
- portserial.c for serial initialisation and interface
- portevent.c for the inter-function event exchange
Step 1: set portserial.c
a) the first function to implement is
Code:
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
The function takes two parameters to enable/disable RX and TX respectively. FMB enables TX only when it really needs to transmit, so when the function is called to enable TX, we also "simulate" the TX buffer empty signal.
Code:
void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
if (xTxEnable)
pxMBFrameCBTransmitterEmpty( );
}
b) now implement the callback function that FMB calls to put a byte in the serial port.
Code:
BOOL xMBPortSerialPutByte( CHAR ucByte )
{
CDC_Device_SendByte(&VirtualSerial_CDC_Interface,ucByte);
xMBPortEventPost(EV_FRAME_SENT);
return TRUE;
}
Note that, after putting a byte in the USB buffer, we also send a FRAME_SENT event. This is used, again, to "simulate" the TX buffer empty signal. We will use it later.
c) finally, define the function called to get a byte from the serial port:
Code:
BOOL xMBPortSerialGetByte( CHAR * pucByte )
{
if (CDC_Device_BytesReceived(&VirtualSerial_CDC_Interface) > 0)
{
int16_t data = CDC_Device_ReceiveByte(&VirtualSerial_CDC_Interface);
if (data >= 0)
*pucByte = (uint8_t)(data);
return TRUE;
}
return FALSE;
}
Not much to say here. Instead of reading from serial we read from USB.
Step 2: changes to portevent.c
The file defined in the BARE port is almost ok. There is only one change to do. You remember that, after sending a byte to USB, we also put a FRAME_SENT event in the FMB queue. It's now time to handle this event:
Code:
BOOL xMBPortEventGet( eMBEventType * eEvent )
{
BOOL xEventHappened = FALSE;
if( xEventInQueue )
{
*eEvent = eQueuedEvent;
xEventInQueue = FALSE;
xEventHappened = TRUE;
// add these two lines
if (eQueuedEvent == EV_FRAME_SENT)
pxMBFrameCBTransmitterEmpty( );
}
return xEventHappened;
}
This will retrigger another fake "TX buffer empty" event and force FMB to send a new byte, if available.
Step 3: implement USB task
You need to periodically call LUFA functions CDC_Device_USBTask() and USB_USBTask() from inside a task or a timer ISR. Short before/after these functions are invoked, check if new bytes are received and in case, inform FMB:
Code:
if (CDC_Device_BytesReceived(&VirtualSerial_CDC_Interface) > 0)
pxMBFrameCBByteReceived( );
CDC_Device_USBTask(&VirtualSerial_CDC_Interface);
USB_USBTask();
That's all. When you call eMBInit(), you can pass any value for serial speed.
These modifications were tested with ASCII MODBUS on a MEGA16U4 and are working well. As soon as the new hardware is ready, I will test on XMEGA64A3U but no big surprises are expected.
If you find mistakes or parts that are not clear, please let me know. |