diff --git a/yaMBSiavr.c b/yaMBSiavr.c new file mode 100644 index 0000000..cd87f9c --- /dev/null +++ b/yaMBSiavr.c @@ -0,0 +1,428 @@ +/************************************************************************* +Title: Yet another (small) modbus (server) implementation for the avr. +Author: Max Brueggemann +Hardware: any AVR with hardware UART, tested on Atmega 88/168 at 20Mhz +License: BSD-3-Clause + +DESCRIPTION: + Refer to the header file yaMBSiavr.h. + +USAGE: + Refer to the header file yaMBSiavr.h. + +LICENSE: + +Copyright 2017 Max Brueggemann, www.maxbrueggemann.de + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +*************************************************************************/ + +#include +#include "yaMBSiavr.h" +#include + +volatile unsigned char BusState = 0; +volatile uint16_t modbusTimer = 0; +volatile unsigned char rxbuffer[MaxFrameIndex+1]; +volatile uint16_t DataPos = 0; +volatile unsigned char PacketTopIndex = 7; +volatile unsigned char modBusStaMaStates = 0; +volatile uint16_t modbusDataAmount = 0; +volatile uint16_t modbusDataLocation = 0; + +/* @brief: save address and amount +* +*/ +void modbusSaveLocation(void) +{ + modbusDataLocation=(rxbuffer[3]|(rxbuffer[2]<<8)); + if (rxbuffer[1]==fcPresetSingleRegister || rxbuffer[1]==fcForceSingleCoil) modbusDataAmount=1; + else modbusDataAmount=(rxbuffer[5]|(rxbuffer[4]<<8)); +} + +/* @brief: returns 1 if data location adr is touched by current command +* +* Arguments: - adr: address of the data object +* +*/ +uint8_t modbusIsInRange(uint16_t adr) +{ + if((modbusDataLocation <= adr) && (adr<(modbusDataLocation+modbusDataAmount))) + return 1; + return 0; +} + +/* @brief: returns 1 if range of data locations is touched by current command +* +* Arguments: - startAdr: address of first data object in range +* - lastAdr: address of last data object in range +* +*/ +uint8_t modbusIsRangeInRange(uint16_t startAdr, uint16_t lastAdr) +{ + if(modbusIsInRange(startAdr) && modbusIsInRange(lastAdr)) + return 1; + return 0; +} + +uint8_t modbusGetBusState(void) +{ + return BusState; +} + +#if ADDRESS_MODE == SINGLE_ADR +volatile unsigned char Address = 0x00; +uint8_t modbusGetAddress(void) +{ + return Address; +} + +void modbusSetAddress(unsigned char newadr) +{ + Address = newadr; +} +#endif + +#if PHYSICAL_TYPE == 485 +void transceiver_txen(void) +{ + TRANSCEIVER_ENABLE_PORT|=(1<>= 1; + if (carry) out ^= 0xA001; + } + } + //out=0x1234; + if ((ptrToArray[inputSize]==out%256) && (ptrToArray[inputSize+1]==out/256)) //check + { + return 1; + } else { + ptrToArray[inputSize]=out%256; //append Lo + ptrToArray[inputSize+1]=out/256; //append Hi + return 0; + } +} + +/* @brief: copies a single bit from one char to another char (or arrays thereof) +* +* +*/ +void listBitCopy(volatile uint8_t *source, uint16_t sourceNr,volatile uint8_t *target, uint16_t targetNr) +{ + if(*(source+(sourceNr/8))&(1<<(sourceNr-((sourceNr/8)*8)))) + { + *(target+(targetNr/8))|=(1<<(targetNr-((targetNr/8)*8))); + } else *(target+(targetNr/8))&=~(1<<(targetNr-((targetNr/8)*8))); +} + +/* @brief: Back to receiving state. +* +*/ +void modbusReset(void) +{ + BusState=(1< perform crc check + modbusSaveLocation(); + BusState=(1<MaxFrameIndex) modbusReset(); + else + { + rxbuffer[DataPos]=data; + DataPos++; //TODO: maybe prevent this from exceeding 255? + } + } else + if (!(BusState & (1<> 8); + UBRRL = (unsigned char) _UBRR; + UART_STATUS = (1<> 8); + *(outreg+1+c*2) = (uint8_t)(*(inreg+c)); + } +} + +/* @brief: copies a single or multiple 16-bit-words from one array of integers to an array of bytes +* +*/ +void modbusRegisterToInt(volatile uint8_t *inreg, volatile uint16_t *outreg, uint8_t amount) +{ + for (uint8_t c=0; c=startAddress) && ((startAddress+size)>=(modbusDataAmount+modbusDataLocation))) { + + if ((rxbuffer[1]==fcReadHoldingRegisters) || (rxbuffer[1]==fcReadInputRegisters) ) + { + if ((modbusDataAmount*2)<=(MaxFrameIndex-4)) //message buffer big enough? + { + rxbuffer[2]=(unsigned char)(modbusDataAmount*2); + intToModbusRegister(ptrToInArray+(modbusDataLocation-startAddress),rxbuffer+3,modbusDataAmount); + modbusSendMessage(2+rxbuffer[2]); + return 1; + } else modbusSendException(ecIllegalDataValue); + } + else if (rxbuffer[1]==fcPresetMultipleRegisters) + { + if (((rxbuffer[6])>=modbusDataAmount*2) && ((DataPos-9)>=rxbuffer[6])) //enough data received? + { + modbusRegisterToInt(rxbuffer+7,ptrToInArray+(modbusDataLocation-startAddress),(unsigned char)(modbusDataAmount)); + modbusSendMessage(5); + return 1; + } else modbusSendException(ecIllegalDataValue);//too few data bytes received + } + else if (rxbuffer[1]==fcPresetSingleRegister) + { + modbusRegisterToInt(rxbuffer+4,ptrToInArray+(modbusDataLocation-startAddress),1); + modbusSendMessage(5); + return 1; + } + //modbusSendException(ecSlaveDeviceFailure); //inapropriate call of modbusExchangeRegisters + return 0; + } else { + modbusSendException(ecIllegalDataValue); + return 0; + } +} + +/* @brief: Handles single/multiple input/coil reading and single/multiple coil writing. +* +* Arguments: - ptrToInArray: pointer to the user's data array containing bits +* - startAddress: address of the first bit in the supplied array +* - size: input array size in the requested format (bits) +* +*/ +uint8_t modbusExchangeBits(volatile uint8_t *ptrToInArray, uint16_t startAddress, uint16_t size) +{ + if ((modbusDataLocation>=startAddress) && ((startAddress+size)>=(modbusDataAmount+modbusDataLocation))) + { + if ((rxbuffer[1]==fcReadInputStatus) || (rxbuffer[1]==fcReadCoilStatus)) + { + if (modbusDataAmount<=((MaxFrameIndex-4)*8)) //message buffer big enough? + { + rxbuffer[2]=(modbusDataAmount/8); + if (modbusDataAmount%8>0) + { + rxbuffer[(uint8_t)(modbusDataAmount/8)+3]=0x00; //fill last data byte with zeros + rxbuffer[2]++; + } + for (uint16_t c = 0; c=modbusDataAmount) && ((DataPos-9)>=rxbuffer[6])) //enough data received? + { + for (uint16_t c = 0; c +/** + * @code #include @endcode + * + * @brief Interrupt-based Modbus implementation for small avr microcontrollers. + * The Modbus implementation guidelines at modbus.org call for response + * timeouts in the range of several seconds , hence only timing critical + * parts have been implemented within ISRs. The actual handling of the Modbus + * frame can easily be done in the main while loop. + * + * @author Max Brueggemann www.maxbrueggemann.de + */ + +/* define baudrate of modbus */ +#ifndef BAUD +#define BAUD 38400L +#endif + +/* +* Definitions for transceiver enable pin. +*/ +#ifndef TRANSCEIVER_ENABLE_PORT +#define TRANSCEIVER_ENABLE_PORT PORTA +#endif + +#ifndef TRANSCEIVER_ENABLE_PIN +#define TRANSCEIVER_ENABLE_PIN 6 +#endif + +#ifndef TRANSCEIVER_ENABLE_PORT_DDR +#define TRANSCEIVER_ENABLE_PORT_DDR DDRA +#endif + +/** + * @brief + * At the moment the user has to set the value for Baudrate and + * speed mode manually. The values depend on the operating frequency + * of your AVR and can be found in its datasheet. + */ +#if defined(__AVR_ATtiny2313__) +#define UART_TRANSMIT_COMPLETE_INTERRUPT USART_TX_vect +#define UART_RECEIVE_INTERRUPT USART_RX_vect +#define UART_TRANSMIT_INTERRUPT USART_UDRE_vect +#define UART_STATUS UCSRA +#define UART_CONTROL UCSRB +#define UART_DATA UDR +#define UART_UDRIE UDRIE + +#elif defined(__AVR_ATmega164P__) +#define UART_TRANSMIT_COMPLETE_INTERRUPT USART1_TX_vect +#define UART_RECEIVE_INTERRUPT USART1_RX_vect +#define UART_TRANSMIT_INTERRUPT USART1_UDRE_vect +#define UART_STATUS UCSR1A +#define UART_CONTROL UCSR1B +#define UART_DATA UDR1 +#define UART_UDRIE UDRIE1 +#define UCSRC UCSR1C +#define RXCIE RXCIE1 +#define TXCIE TXCIE1 +#define RXEN RXEN1 +#define TXEN TXEN1 +#define UCSZ0 UCSZ10 +#define U2X U2X1 +#define UBRRH UBRR1H +#define UBRRL UBRR1L + +#elif defined(__AVR_ATmega168PA__)|(__AVR_ATmega88PA__)|(__AVR_ATmega328P__)|(__AVR_ATmega168P__)|(__AVR_ATmega88P__) +#define UART_TRANSMIT_COMPLETE_INTERRUPT USART_TX_vect +#define UART_RECEIVE_INTERRUPT USART_RX_vect +#define UART_TRANSMIT_INTERRUPT USART_UDRE_vect +#define UART_STATUS UCSR0A +#define UART_CONTROL UCSR0B +#define UART_DATA UDR0 +#define UART_UDRIE UDRIE0 +#define UCSRC UCSR0C +#define RXCIE RXCIE0 +#define TXCIE TXCIE0 +#define RXEN RXEN0 +#define TXEN TXEN0 +#define UCSZ0 UCSZ00 +#define U2X U2X0 +#define UBRRH UBRR0H +#define UBRRL UBRR0L + +#elif defined(__AVR_ATmega328PB__) +#define UART_TRANSMIT_COMPLETE_INTERRUPT USART0_TX_vect +#define UART_RECEIVE_INTERRUPT USART0_RX_vect +#define UART_TRANSMIT_INTERRUPT USART0_UDRE_vect +#define UART_STATUS UCSR0A +#define UART_CONTROL UCSR0B +#define UART_DATA UDR0 +#define UART_UDRIE UDRIE0 +#define UCSRC UCSR0C +#define RXCIE RXCIE0 +#define TXCIE TXCIE0 +#define RXEN RXEN0 +#define TXEN TXEN0 +#define UCSZ0 UCSZ00 +#define U2X U2X0 +#define UBRRH UBRR0H +#define UBRRL UBRR0L + +#elif defined(__AVR_ATtiny441__) +#define UART_TRANSMIT_COMPLETE_INTERRUPT USART0_TX_vect +#define UART_RECEIVE_INTERRUPT USART0_RX_vect +#define UART_TRANSMIT_INTERRUPT USART0_UDRE_vect +#define UART_STATUS UCSR0A +#define UART_CONTROL UCSR0B +#define UART_DATA UDR0 +#define UART_UDRIE UDRIE0 +#define UCSRC UCSR0C +#define RXCIE RXCIE0 +#define TXCIE TXCIE0 +#define RXEN RXEN0 +#define TXEN TXEN0 +#define UCSZ0 UCSZ00 +#define U2X U2X0 +#define UBRRH UBRR0H +#define UBRRL UBRR0L + +#elif defined(__AVR_ATmega8__)|| defined(__AVR_ATmega16__) || defined(__AVR_ATmega32__) || defined(__AVR_ATmega323__) +#define UART_TRANSMIT_COMPLETE_INTERRUPT USART_TXC_vect +#define UART_RECEIVE_INTERRUPT USART_RXC_vect +#define UART_TRANSMIT_INTERRUPT USART_UDRE_vect +#define UART_STATUS UCSRA +#define UART_CONTROL UCSRB +#define UART_DATA UDR +#define UART_UDRIE UDRIE + +#elif defined(__AVR_AT90PWM3B__) +#define UART_TRANSMIT_COMPLETE_INTERRUPT USART_TX_vect +#define UART_RECEIVE_INTERRUPT USART_RX_vect +#define UART_TRANSMIT_INTERRUPT USART_UDRE_vect +#define UART_STATUS UCSRA +#define UART_CONTROL UCSRB +#define UART_DATA UDR +#define UART_UDRIE UDRIE + +#elif defined(__AVR_ATmega1284P__) +#define UART_TRANSMIT_COMPLETE_INTERRUPT USART0_TX_vect +#define UART_RECEIVE_INTERRUPT USART0_RX_vect +#define UART_TRANSMIT_INTERRUPT USART0_UDRE_vect +#define UART_STATUS UCSR0A +#define UART_CONTROL UCSR0B +#define UART_DATA UDR0 +#define UART_UDRIE UDRIE0 +#define UCSRC UCSR0C +#define RXCIE RXCIE0 +#define TXCIE TXCIE0 +#define RXEN RXEN0 +#define TXEN TXEN0 +#define UCSZ0 UCSZ00 +#define U2X U2X0 +#define UBRRH UBRR0H +#define UBRRL UBRR0L + +#else +#error "no definition available" +#endif + +#ifndef F_CPU +#error " F_CPU not defined " +#else + #define _UBRR (F_CPU / 8 / BAUD ) -1 +#endif /* F_CPU */ +/* + * Available address modes. +*/ +#define MULTIPLE_ADR 2 +#define SINGLE_ADR 1 + +/* +* Use SINGLE_ADR or MULTIPLE_ADR, default: SINGLE_ADR +* This is useful for building gateways, routers or clients that for whatever reason need multiple addresses. +*/ +#define ADDRESS_MODE SINGLE_ADR + +/* +* Use 485 or 232, default: 485 +* Use 232 for testing purposes or very simple applications that do not require RS485 and bus topology. +*/ +#define PHYSICAL_TYPE 485 //possible values: 485, 232 + + +#if BAUD>=19200 +#define modbusInterFrameDelayReceiveStart 16 +#define modbusInterFrameDelayReceiveEnd 18 +#define modbusInterCharTimeout 7 +#else +#define modbusBlocksize 10 +#define modbusBlockTime ((float)modbusBlocksize*1000000)/((float) BAUD) //is 260 for 38400 +#define timerISROccurenceTime 100 //time in microseconds between two calls of modbusTickTimer +#define modbusInterFrameDelayReceiveStart (uint16_t)(modbusBlockTime*3.5/(float)timerISROccurenceTime) +#define modbusInterFrameDelayReceiveEnd (uint16_t)(modbusBlockTime*4/(float)timerISROccurenceTime) +#define modbusInterCharTimeout (uint16_t)(modbusBlockTime*1.5/(float)timerISROccurenceTime) +#endif + +/** + * @brief Defines the maximum Modbus frame size accepted by the device. 255 is the default + * and also the maximum value. However, it might be useful to set this to lower + * values, with 8 being the lowest possible value, in order to save on ram space. + */ +#define MaxFrameIndex 255 + +/** + * @brief Modbus Function Codes + * Refer to modbus.org for further information. + * It's good practice to return exception code 01 in case you receive a function code + * that you haven't implemented in your application. + */ +#define fcReadCoilStatus 1 //read single/multiple coils +#define fcReadInputStatus 2 //read single/multiple inputs +#define fcReadHoldingRegisters 3 //read analog output registers +#define fcReadInputRegisters 4 //read analog input registers (2 Bytes per register) +#define fcForceSingleCoil 5 //write single bit +#define fcPresetSingleRegister 6 //write analog output register (2 Bytes) +#define fcForceMultipleCoils 15 //write multiple bits +#define fcPresetMultipleRegisters 16 //write multiple analog output registers (2 Bytes each) +#define fcReportSlaveID 17 //read device description, run status and other device specific information + +/** + * @brief Modbus Exception Codes + * Refer to modbus.org for further information. + * It's good practice to return exception code 01 in case you receive a function code + * that you haven't implemented in your application. + */ +#define ecIllegalFunction 1 +#define ecIllegalDataAddress 2 +#define ecIllegalDataValue 3 +#define ecSlaveDeviceFailure 4 +#define ecAcknowledge 5 +#define ecSlaveDeviceBusy 6 +#define ecNegativeAcknowledge 7 +#define ecMemoryParityError 8 + +/** + * @brief Internal bit definitions + */ +#define BusTimedOut 0 +#define Receiving 1 +#define Transmitting 2 +#define ReceiveCompleted 3 +#define TransmitRequested 4 +#define TimerActive 5 +#define GapDetected 6 + +/** +* @brief Configures the UART. Call this function only once. +*/ +extern void modbusInit(void); + +/** +* @brief receive/transmit data array +*/ +extern volatile unsigned char rxbuffer[MaxFrameIndex+1]; + +/** +* @brief Current receive/transmit position +*/ +extern volatile uint16_t DataPos; + +/** + * This only applies to single address mode. + */ +#if ADDRESS_MODE == SINGLE_ADR + /** + * @brief: Read the device address + */ + extern uint8_t modbusGetAddress(void); + + /** + * @brief: Set the device address + * Arguments: - newadr: the new device address + */ + extern void modbusSetAddress(unsigned char newadr); +#endif + +/* @brief: Sends a response. +* +* Arguments: - packtop, index of the last byte in rxbuffer +* that contains payload. Maximum value is +* MaxFrameIndex-2. +*/ +extern void modbusSendMessage(unsigned char packtop); + +/* @brief: Sends a Modbus exception. +* +* Arguments: - exceptionCode +*/ +extern void modbusSendException(unsigned char exceptionCode); + +/* @brief: Discards the current transaction. For MULTIPLE_ADR-mode and general +* testing purposes. Call this function if you don't want to reply at all. +*/ +void modbusReset(void); + +/** + * @brief Call this function whenever possible and check if its return value has the ReceiveCompleted Bit set. + * Preferably do this in the main while. I do not recommend calling this function within ISRs. + * @example if (modbusGetBusState() & (1<