[Series Hướng dẫn lập trình Modbus cho vi điều khiển]
Lâu lâu mình lại bận bịu vài tháng, thậm chí cả năm mới quay lại viết lách. Phần 1 thì đã có từ lâu rồi và rất nhiều bạn đang hóng phần 2 để dò xét đúng sai 😀
Phần này mình sẽ giới thiệu cho các bạn 1 thư viện miễn phí và đầy đủ, đó là: FreeModbus, các bạn truy cập link dưới để tải về: https://sourceforge.net/projects/freemodbus.berlios/ Trước kia khi mình viết phần 1 thì vẫn còn trang chủ freemodbus.org để truy cập và tìm tài liệu, nhưng giờ trang đấy đã bị điều hướng sang 1 trang khác và mình không tìm thấy mục tải cũng như tài liệu. có vẻ tên miền đó đã bị mua lại. Nhưng không sao, các bạn tải trên link trên là được.
Trong mục “demo” sẽ có vị dụ demo về cách sử dụng modbus cho các nền tảng khác nhau như ATMEGA, STM32, MPS430, hệ điều hành window, hệ điều hành linux…vân vân và mây mây.Thư mục “modbus” chứa tất cả code modbus ở đó:
+ Thư mục “ascii” chức các file *.c phục vụ chạy modbus – ascii+ Thư mục “rtu” chứa các file *.c phục chạy modbus – rtu+ Thư mục “tcp” chức các file *.c phục vụ chạy modbus tcp+ Thư mục “functions” chứa các file *.c thực hiện các lệnh trong modbus ví dụ: Read coils, Write Coils, ReadHolding, WriteHolding ..v.v..+ Thư mục “include” chứa tất cả các file *.h, nếu bạn sử dụng keilC thì trỏ đường dẫn đến thư mục nàyNếu sử dụng modbus – ascii thì không cần thiết thêm các file thuộc thư mục rtu hoặc tcp vào project, nếu bạn muốn thêm thì cũng chả sao.
Trong đống file trên bạn sẽ cần sửa 2 file rất quan trọng, đó là file “portserial.c” và file “porttimer.c”, bởi vì 2 file này là 2 file dùng để giao tiếp giữa thư viện modbus và con chip mà bạn đang lập trình. Ứng với mỗi chip khác nhau thì 2 file trên sẽ có nội dung khác nhau. Do đó bạn sẽ phải tự viết 2 file này . Đừng lo, mình sẽ hướng dẫn. Phần này mình sẽ chạy ở chế độ Slave nên các file sẽ chỉnh sửa để chạy theo kiểu slave.
portserial.c
portserial.c là file giúp thư viện modbus điều khiển được việc giao tiếp UART/USART trên chip của bạn. Bạn cần phải sửa hết nội dung của file này, cụ thể là các hàm sau:
+ void vMBPortSerialEnable
Hàm này dùng để bật/tắt ngắt USART. Hàm này rất quan trọng, bởi vì nó điều phối hoạt động gửi/ nhận dữ liệu cho modbus. Trên đường truyền modbus thì hoạt động gửi / nhận sẽ diễn ra luân phiên. Hàm này sẽ được dùng thường xuyên để bật/ tắt 1 trong 2 chức năng truyền và nhận. Và 1 điều RẤT RẤT quan trọng về hàm này là bạn phải set cờ ngắt truyền lên mức 1 để đảm bảo chương trình sẽ nhảy vào ngắt truyền ngay lập tức khi Enable chức năng truyền lên. Vì sao ? vì hoạt động truyền dữ liệu được điều khiển hoàn toàn bởi ngắt truyền, nếu lần ngắt đầu tiên không hoạt động thì sẽ không diễn ra việc gửi dữ liệu. Và modbus đương nhiên không hoạt động. Bạn vẫn đang khó hiểu phải không ? Hãy tưởng tượng bạn có 1 mảng dữ liệu cần gửi đi, cách thông thường là bạn gửi từng byte trong mảng đấy và chờ từng byte trong mảng đấy gửi xong rồi mới gửi byte mới và tiếp tục cho đến hết. Như vậy bạn sẽ mất thời gian ở việc chờ gửi byte, và nếu số lượng byte lớn thì việc chờ sẽ càng lâu, mà việc chờ sẽ khiến chương trình chạy chậm lại. Rất là không hợp lý. Do đó các nhà phát triển ứng dụng FreeModbus sử dụng ngắt truyền để việc gửi dữ liệu diễn ra tự động, không phải chờ, chương trình không bị chạy chậm lại. Bằng cách: mỗi lần ngắt truyền sẽ gửi 1 byte, sau khi byte đó được gửi xong, hệ thống sẽ tạo ra ngắt, và ngắt đó lại gửi byte khác cho đến hết. Cho nên tôi mới lưu ý quan trọng là : SAU khi Enable chức năng truyền thì phải đảm bảo có ngắt truyền diễn ra, vì không có phát ngắt truyền đó thì việc truyền dữ liệu sẽ không thể diễn ra được.Ví dụ về đoạn code trên stm32 như sau:
Ở STM32, câu lệnh USART_ITconfig(EMB_COM, USART_IT_TXE, ENABLE) sẽ mở chức năng ngắt truyền và đồng thời sét cờ ngắt để nhảy vào hàm ngắt truyền luôn.Còn lệnh USART_ITconfig(EMG_COM, USART_IT_RXNE, ENABLE) là mở chức năng ngắt nhận.
+ BOOL xMBPortSerialInit
Hàm xMBPortSerialInit dùng để khởi tạo chức năng USART cho chip, cái này thì ai cũng biết rồi, cái này tùy bạn cấu hình thôi, miễn là bật được USART lên để thằng FreeModbus sử dụng được.
+ BOOL xMBPortSerialPutByte( CHAR ucByte )
hàm xMBPortSerialPutByte dùng để gửi 1 byte qua USART, cái này mỗi chip 1 khác nên bạn tự viết. Nhưng phải nhớ 1 điều rằng: KHÔNG ĐƯỢC CHỜ GỬI BYTE XONGTại vì đã dùng ngắt để tối ưu chỗ chờ gửi byte rồiVí dụ với STM32:
+ BOOL xMBPortSerialGetByte
Hàm xMBPortSerialGetByte dùng để đọc 1 byte nhận về từ USART, bạn tự sửa theo chip của bạn. Hàm này thì dễ rồi, từng làm với USART thì ai cũng biết rồi.Ví dụ với STM32:
Hàm ngắt nhận dữ liệu
Bạn cần viết 1 hàm ngắt nhận dữ liệu từ USART, bạn nào chưa hiểu hàm ngắt nhận là gì thì vui lòng học USART trước nha. Trong hàm ngắt nhận USART thì bạn gọi hàm pxMBFrameCBByteReceived( ) vào. Ví dụ:
Hàm ngắt truyền dữ liệu
Tiếp theo, bạn cần viết 1 hàm ngắt truyền USART, hàm ngắt truyền sẽ xảy ra khi gửi xong 1 byte qua USART, hoặc xảy ra khi vừa ENABLE chức năng ngắt truyền. Cái này thì bạn tự viết theo chip mà bạn đang sử dụng. Miễn là trong hàm ngắt truyền bạn gọi hàm pxMBFrameCBTransmitterEmpty( ).Ví dụ:
Thế đó, file portserial.c này sửa có vậy thôi, Thật ra bạn đọc chỗ trên này chỉ để hiểu thôi, chứ bạn có thể lên internet tìm file đó, vì trên internet có gần hết rồi, chỉ cần tìm từ khóa “portserial freemodbus với chip xxx”. xxx là tên chip của bạn. Hoặc là bạn vào thư mục “demo” mà bạn có sau khi giải nén thư viện freemodbus. Tìm trong đống demo đó khả năng cao sẽ có file cho đúng loại chip mà bạn cần. Tiếp theo mình sẽ hướng dẫn cách sửa file “porttimer.c”
porttimer.c
file này để làm gì ?File này dùng để cấu hình 1 timer mà freemodbus dùng để xác định khi nào nhận được đầy đủ 1 lệnh (command) qua serial. Vậy, Tại sao lại có thể phát hiện đã nhận hết lệnh qua USART bằng TIMER ? WTH?Hãy nhìn bức ảnh dưới này, thể hiện các lệnh được gửi nối tiếp nhau qua serial.
Xem hình 4. Các lệnh command1, command2, command3, command4 được gửi lần lượt qua serial. Khoảng thời gian giữa các command gọi là T2, còn khoảng thời gian giữa các byte cùng 1 command là T1. Dễ thấy rằng thời gian rảnh rỗi giữa các lệnh lớn hơn thời gian rảnh rỗi giữa 2 byte cùng 1 lệnh. Do đó freemodbus sử dụng timer để phát hiện khi nào nhận được byte cuối cùng của 1 lệnh nào đó. Cụ thể, khi có ngắt nhận được 1 byte nào đó, thì bật timer lên, nếu có lần ngắt nhận byte tiếp theo trước khi có ngắt timer thì chứng tỏ byte mới nhận chưa phải byte cuối cùng. Còn nếu có ngắt timer xảy ra mà chưa có ngắt nhận byte tiếp thì chứng tỏ byte này là byte cuối cùng của command.Để hoạt động đúng thì thời gian từ lúc bật timer đến lúc xảy ra ngắt timer không được lớn hơn T2.
Sau đây là phần hướng dẫn cấu hình, chúng ta sẽ cần phải sửa hàm cấu hình timer, để đạt được thời gian tràn hợp lý nhất
+ BOOL xMBPortTimersInit( USHORT TimerFrq )
Hàm xMBPortTimersInit dùng để khởi tạo Timer, hàm này mình đã sửa lại nên mình để tham số đầu vào là tần số Timer cho tiện. Vậy, nên để tần số ngắt timer bao nhiêu ? Trước tiên ta cần biết thời gian để gửi 1 byte qua serial là bao nhiêu? Tôi gọi thời gian để gửi 1 byte qua serial (TB) = 8/ Baudrate (giây)Ví dụ Baudrate = 19200 (bps) thì TB = 8/19200 = 416.6 (micro giây)Ta đã tính được TB rồi, vậy câu hỏi bây giờ là cấu hình tần số ngắt của timer là bao nhiêu ?Freemodbus khuyến cáo chúng ta nên để khoảng thời gian từ lúc bật timer cho đến khi timer tràn (Ti) = 3.5 * TB.Vậy ở đây bạn cấu hình làm sao cho Ti = 3.5* TB là được. TB thì bạn biết lấy đâu ra rồi đó. Ví dụ, code cho STM32:
+ void vMBPortTimersEnable( void )
Hàm vMBPortTimersEnable dùng để bật chức năng ngắt timer, bạn viết làm sao để khi gọi hàm này thì chức năng ngắt timer được bật là được. Mỗi khi bật chức năng ngắt timer thì bạn nhớ reset couter trước để đảm bảo đúng thời gian ngắt.ví dụ với STM32:
void vMBPortTimersDisable( void )
Hàm vMBPortTimersDisable dùng để tắt tính năng ngắt timer, bạn viết làm sao cho khi gọi hàm này thì timer sẽ không thể ngắt được nữa là được.Ví dụ với STM32:
Hàm ngắt timer
Cấu hình timer từ nãy rồi, vậy thì phải có hàm ngắt timer chứ nhỉ, trong hàm ngắt timer của bạn phải gọi hàm pxMBPortCBTimerExpired( ) mới được.Ví dụ:
Vậy là xong phần sửa 2 file “portserial.c” và “porttimer.c”, chúc mừng các bạn đã đọc đến đây. Nghĩ thôi cũng thấy oải rồi. Nhưng chương trình vẫn còn tiếp !
ModbusUser.c
Tiếp theo bạn cần tạo 1 file ModbusUser.c vào project của bạn và copy đoạn code bên dưới này vào. Phần code này là do mình viết để sử dụng được thư viện modbus. LƯU Ý RẰNG: FILE này chỉ cần khi sử dụng modbus ở chế độ SLAVE. Tức cài đặt thiết bị này là Slave.
File ModbusUser.c cung cấp cho bạn các hàm thao tác lên dữ liệu modbus, và hàm Khởi tạo tổng quan cho Modbus. Ngoài ra trên file này còn khai báo 1 số thanh ghi dùng chung đã nhắc đến ở phần 1. Cụ thể là:
+ usRegInputBuf: Là thanh ghi dùng chung 16 bit (mình gọi là REG_INPUT)+ usRegHoldingBuf: là thanh ghi chung 16 bit (mình gọi là HOLDING)+ usCoilBuf: là thanh ghi dùng chung 1bit (còn gọi là thanh ghi COIL)+ usDescreteBuf: là thanh ghi dùng chung 1 bit chỉ đọc (bởi master) gọi là thanh ghi (DESCRETES_INPUT).Các bạn lưu ý rằng file này chỉ dùng cho chế độ SLAVE. và việc khai báo các thanh ghi này chỉ khai báo trên slave.
Giải thích các hàm trong ModbusUser.c
eMBErrorCode UseModbus(void)
Hàm này để khởi tạo sử dụng Modbus, Hiện tại hàm này đang hỗ trợ khởi tạo sử dụng Modbus ASCII hoặc modbus RTU. Phần cấu hình này mình sẽ hướng dẫn sau.
eMBAccessDataCode MBSetCoil(USHORT bitOfset, char value)
Hàm MBSetCoil dùng để đặt 1 giá trị cho 1 coil của thanh ghi chung COIL tại địa chỉ bitOfset, địa chỉ này phải > 0, value là giá trị đặt cho Coil đó, có 2 giá trị cho phép đó là 0 hoặc 1. Hàm này trả về kiểu giá trị eMBAccessDataCode, kiểu giá trị này có cấu trúc như sau:
typedef enum { ACCESS_ADDR_ERR, // Lỗi địa chỉ truy cập ACCESS_NO_ERR, // Không hề có lỗi ACCESS_WRONG_DATA_TYPE // Lỗi sai kiểu dữ liệu modbus } eMBAccessDataCode;
Ví dụ cách dùng: MBSetCoil(1, 1); // Set giá trị 1 cho thanh ghi chung COIL tại địa chỉ 1 MBSetCoil(2,0); // Set giá trị 0 cho thanh ghi chung COIL tại địa chỉ 2 hoặc: eMBAccessDataCode stt = MBSetCoil(200, 1); if (stt == ACCESS_NO_ERR) { println(“Set coil successful”); }
eMBAccessDataCode MBGetCoil(USHORT bitOfset, UCHAR* GetValue)
Hàm MBGetCoil dùng để lấy 1 byte từ thanh ghi chung COIL. ví dụ:
eMBAccessDataCode MBSetDescretesInputbit(USHORT bitOfset, char value)
Hàm SetDescretesInputbit dùng để set 1 giá trị cho thanh ghi chung DescretesInput tại địa chỉ bitOfset. Ví dụ set giá trị 1 tại địa chỉ 120:
eMBAccessDataCode MBGetDescretesInputbit(USHORT bitOfset, UCHAR* GetValue)
Hàm MBGetDescretesInputbit dùng để lấy giá trị của thanh ghi chung DescretesInput tại địa chỉa bitOfset. Ví dụ lấy giá trị tại địa chỉ 10:
eMBAccessDataCode MBGetData16Bits(MB_Data_Type DataType, USHORT Address, USHORT* Value)
Hàm GetData16Bits dùng để lấy ra giá trị 16bit của thanh ghi chung HOLDING hoặc thanh ghi INPUT tại địa chỉ Address, và giá trị được truyền ra tham số con trỏ *Value
Ví dụ lấy giá trị thanh ghi HOLDING tại địa chỉ 85:
Ví dụ lấy giá trị thanh ghi INPUT tại địa chỉ 12:
eMBAccessDataCode MBSetData16Bits(MB_Data_Type DataType, USHORT Address, USHORT Value)
Hàm MBSetData16Bits dùng để set giá trị cho thanh ghi chung HOLDING hoặc thanh ghi chung INPUT tại địa chỉ Address, với giá trị Value. Ví dụ set giá trị 3000 cho thanh ghi HOLDING tại địa chỉ 20:
Ví dụ set giá trị 1500 cho thanh ghi INPUT tại địa chỉ 15:
eMBAccessDataCode MBGetData32Bits(MB_Data_Type DataType, USHORT Address, ULONG* Value)
Hàm MBGetData32Bits dùng để lấy giá trị 32bit từ thanh ghi chung HOLDING hoặc INPUT tại địa chỉ Address, và dữ liệu được truyền ra tham số con trỏ *Value. Ví dụ lấy dữ liệu 32bit từ thanh ghi HOLDING tại địa chỉ 50:
Ví dụ lấy dữ liệu 32bit từ thanh ghi INPUT tại địa chỉ 200:
eMBAccessDataCode MBSetData32Bits(MB_Data_Type DataType, USHORT Address, ULONG Value)
Hàm MBSetData32Bits dùng để set giá trị 32 bit(Value) cho thanh ghi chung HOLDING hoặc INPUT tại địa chỉ Address.
Ví dụ set giá trị 22333444555 cho thanh ghi HOLDING tại địa chỉ 100:
Ví dụ set giá trị -11300876432 cho thanh ghi INPUT tại địa chỉ 1:
eMBAccessDataCode MBSetFloatData(MB_Data_Type DataType, USHORT Address, float Value)
Hàm MBSetFloatData dùng để set 1 giá trị kiểu thực (float) cho thanh ghi chung HOLDING hoặc INPUT tại địa chỉ Address.
Ví dụ set giá trị 20.04 cho thanh ghi HOLDING tại địa chỉ 24:
Ví dụ set giá trị 30.5 cho thanh ghi INPUT tại địa chỉ 25:
eMBAccessDataCode MBGetFloatData(MB_Data_Type DataType, USHORT Address, float* Value)
Hàm MBGetFloatData dùng để lấy dữ liệu kiểu thực (float ) từ thanh ghi chung HOLDING hoặc INPUT tại địa chỉ Address.
Ví dụ lấy dữ liệu tại địa chỉ 10 của thanh ghi HOLDING:
Ví dụ lấy dữ liệu tại địa chỉ 44 của thanh ghi INPUT:
Được rồi, dài quá rồi, đến đây bạn đã có đầy đủ file để chạy được modbus. Nhưng vẫn chưa build được, còn 1 số ý mình sẽ trình bày ở phần 3. Đoạn code trên khá dài, nhưng mình không muốn để link vì sợ sau này link die. các bạn cố gắng copy rồi tạo thành file nhé. Good luck !