本人軟件工程專業(yè),關于硬件只有408的基礎,后續(xù)學習發(fā)現(xiàn)一些博主所講以及b站上所給的教程并不是很清晰,故編寫此文檔供大家查看。
如果我說的地方哪里有問題,希望大家可以給出意見!(●ˇ?ˇ●)
參考文檔:
Modbus通訊協(xié)議常用功能碼解釋_modbus功能碼_Lee139499的博客-CSDN博客
目錄
一.什么是MODBUS RTU
1.關于MODBUS中的功能碼
?2.MODBUS RTU中的數(shù)據(jù)幀結構
?編輯
二.代碼上的實現(xiàn)
1.初始化定時器和USART
2.設置定時器為輸入捕獲模式
3.在USART接收中斷中記錄定時器值
4.自定義文件,針對于MODBUS協(xié)議對數(shù)據(jù)進行處理
三.使用軟件
一.什么是MODBUS RTU
?????????MODBUS是一種單主站的主/從通訊模式。Modbus網(wǎng)絡上只有一個主站,主站在Modbus網(wǎng)絡上沒有地址,從站的地址范圍為0-247,其中0為廣播地址,從站的實際地址范圍為1-247。
????????通信由主機發(fā)起,一問一答式,從機無法主動向主機發(fā)送數(shù)據(jù)。
? ? ? ? 傳輸過程中,兩個字節(jié)之間的相鄰時間不得大于3.5個字符的時間,否則視為一幀數(shù)據(jù)傳輸結束。
1.關于MODBUS中的功能碼
常用的就是01、02、03、04、05、06、15、16,具體描述見下圖:
?2.MODBUS RTU中的數(shù)據(jù)幀結構
????????地址:設備的 MODBUS 地址,用于標識通信中的從設備。
????????功能碼:表示對從設備執(zhí)行的操作,例如讀取保持寄存器、寫單個寄存器等。
????????數(shù)據(jù)(2字節(jié)):傳輸?shù)臄?shù)據(jù),由兩個字節(jié)組成。具體數(shù)據(jù)內(nèi)容可能根據(jù)功能碼不同而有所變化。????????
????????CRC校驗(2字節(jié)):用于驗證數(shù)據(jù)的完整性,由兩個字節(jié)組成。該校驗值是在數(shù)據(jù)幀中的所有字段(包括地址、功能碼和數(shù)據(jù))被計算后得到的。
二.代碼上的實現(xiàn)
????????此項目中,我使用的是STM32F103C8T6開發(fā)板,串口使用USART。
????????因為我只需要實現(xiàn)了03,06功能碼,所以代碼部分只有針對這兩個功能碼的實現(xiàn)。
????????那么,根據(jù)該協(xié)議,我們需要使用定時器來實現(xiàn)判斷兩個字節(jié)之間的相鄰時間,確保數(shù)據(jù)傳輸?shù)臅r間間隔不得大于設定好的時間。
????????在串口USART中判斷兩個字節(jié)之間的相鄰時間,以確保數(shù)據(jù)幀傳輸不超過設定的時間閾值。我們使用一個定時器來記錄兩個字節(jié)之間的時間,并在定時器中斷中進行判斷。
? ? ? ? 步驟如下:
1.初始化定時器和USART
??????首先,你需要初始化定時器和USART,確保它們已經(jīng)配置正確。
void Serial_Init(void){
// 1.開啟時鐘(USART與GPIO)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);// USART
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);// GPIO
// 2.GPIO初始化(TX——復用輸出,RX——輸入)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
// 3.配置USART
USART_InitTypeDef USART_InitStructure = {
.USART_BaudRate = 9600,// 波特率
.USART_HardwareFlowControl = USART_HardwareFlowControl_None, // 硬件流控制
.USART_Mode = USART_Mode_Tx | USART_Mode_Rx,// 指定發(fā)送功能 如果又要發(fā)送也要接收 可以采用 A | B 的格式
.USART_Parity = USART_Parity_No,// 校驗位
.USART_StopBits = USART_StopBits_1,// 停止位
.USART_WordLength = USART_WordLength_8b
};
USART_Init(USART1,&USART_InitStructure);
// 開啟中斷
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
// 配置NVIC
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 初始化NVIC的USART1通道
NVIC_InitTypeDef NVIC_InitStructure = {
.NVIC_IRQChannel = USART1_IRQn,
.NVIC_IRQChannelCmd = ENABLE,
.NVIC_IRQChannelPreemptionPriority = 1,
.NVIC_IRQChannelSubPriority = 1
};
NVIC_Init(&NVIC_InitStructure);
// 4.開啟USART(或配置中斷)
USART_Cmd(USART1,ENABLE);
}
????????以及定時器的相關配置:
void Timer_Init(uint16_t arr,uint16_t psc){
//RCC內(nèi)部時鐘 ON
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
//時鐘源選擇
TIM_InternalClockConfig(TIM3);
//配置時機單元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 不分頻
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上計數(shù)
TIM_TimeBaseInitStructure.TIM_Period = arr ; // 因為預分頻器和計數(shù)器都有1個數(shù)的偏差,所以這里要再減去一個1
TIM_TimeBaseInitStructure.TIM_Prescaler = psc ; // Tout = ((arr+1)*(psc+1))/Tclk ;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM3,TIM_IT_Update);
//配置輸出中斷控制
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);
//配置NVIC
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 優(yōu)先級分組
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM_Channel_3; // 中斷通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 制特定中斷通道的使能狀態(tài)
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 搶占優(yōu)先級
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 響應優(yōu)先級
NVIC_Init(&NVIC_InitStructure);
//啟動定時器
TIM_Cmd(TIM3,ENABLE);
}
2.設置定時器為輸入捕獲模式
????????將定時器設置為輸入捕獲模式,以便在USART接收到一個字節(jié)時記錄定時器的值。
// 配置定時器通道為輸入捕獲模式
void configure_input_capture() {
// 配置輸入捕獲通道 CHx 為輸入捕獲模式
TIM3->CCMR1 |= TIM_CCMR1_CC1S_0; // 將CC1S位設置為01,選擇輸入捕獲通道1為TI1
// 配置輸入捕獲通道 CHx 的觸發(fā)邊沿或狀態(tài)變化條件
TIM3->CCER |= TIM_CCER_CC1P; // 設置捕獲邊沿為下降沿觸發(fā),如果需要上升沿觸發(fā),可以選擇設置為TIM_CCER_CC1NP
// 使能捕獲通道 CHx
TIM3->CCER |= TIM_CCER_CC1E;
}
3.在USART接收中斷中記錄定時器值
????????在USART接收中斷中,記錄定時器的當前值,并在接收到字節(jié)時啟動或重置定時器。
// 定時器中的變量定義:
volatile uint32_t last_capture_time = 0;
const uint32_t max_frame_time = 4000; // 設定的最大幀傳輸時間,單位為定時器計數(shù)值
// 串口中的變量定義:
uint8_t Serial_RxPacket[100] = {0};
uint16_t Serial_RxLength = 0;
uint8_t Serial_RxFlag;
uint8_t clearBufferFlag = 0;
????????以下為定時器的中斷配置:
void TIM3_IRQHandler(void){
if(TIM_GetITStatus(TIM3,TIM_IT_CC3) != RESET){ // 輸入捕獲中斷觸發(fā),計算兩個捕獲之間的時間間隔
uint32_t current_capture_time = TIM_GetCapture1(TIM3);
uint32_t time_interval = current_capture_time - last_capture_time;
if (time_interval > max_frame_time) {
// 超過設定的最大幀傳輸時間,認為一幀數(shù)據(jù)傳輸結束
// 處理完整的數(shù)據(jù)幀
Serial_RxFlag = 1;
}
// 重置定時器捕獲時間
last_capture_time = current_capture_time;
TIM_ClearITPendingBit(TIM3,TIM_IT_CC3);
}
}
????????以下為串口USART的中斷配置:
void USART1_IRQHandler(void){
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
// 接收到一個字節(jié)數(shù)據(jù),記錄定時器的當前值
last_capture_time = TIM_GetCapture3(TIM3);
Serial_RxPacket[Serial_RxLength++] = USART_ReceiveData(USART1);
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1,USART_IT_RXNE);// 清除標志位
}
}
????????在定時器輸入捕獲中斷中判斷兩個字節(jié)之間的時間: 當定時器捕獲到第二個字節(jié)時,計算兩個捕獲之間的時間間隔。如果這個時間間隔超過設定的閾值,則視為一幀數(shù)據(jù)傳輸結束。
4.自定義文件,針對于MODBUS協(xié)議對數(shù)據(jù)進行處理
#include "stm32f10x.h"
#include "Timer.h"
#include "Serial.h"
uint8_t Serial_TxPacket[100] = {0}; // 發(fā)送內(nèi)容
extern uint8_t Serial_RxPacket[100]; // 接收內(nèi)容
extern uint16_t Serial_RxLength;
extern uint16_t modbus_io[100]; // modbus寄存器內(nèi)數(shù)據(jù)
// uint16_t modbus_id = 0X01; // id號
uint16_t modbus_function; // 功能碼
uint16_t modbus_check; // 校驗位
uint16_t modbus_packege_times = 0; // 總包計數(shù)
uint16_t CRC_check_result; // CRC校驗的結果
uint16_t calculate_crc16(const uint8_t *data, size_t len) {
// printf("%d\n",len);
// 初始化crc為0xFFFF
uint16_t crc = 0xFFFF;
// 循環(huán)處理每個數(shù)據(jù)字節(jié)
for (size_t i = 0; i < len; i++) {
// 將每個數(shù)據(jù)字節(jié)與crc進行異或操作
crc ^= data[i];
// 對crc的每一位進行處理:如果最低位為1,則右移一位并執(zhí)行異或0xA001操作(即0x8005按位顛倒后的結果)
for (int j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
}
// 如果最低位為0,則僅將crc右移一位
else {
crc = crc >> 1;
}
}
}
return crc;
}
void Data_Funcion_3(void){
Serial_TxPacket[0] = Serial_RxPacket[0]; // ID
Serial_TxPacket[1] = Serial_RxPacket[1]; // 功能碼
// 字節(jié)長度,根據(jù)接收的內(nèi)容4,5位來判斷
Serial_TxPacket[2] = (Serial_RxPacket[4] << 8 | Serial_RxPacket[5]) * 2;
for(modbus_packege_times = 0;modbus_packege_times<Serial_TxPacket[2];modbus_packege_times+=2)
{
Serial_TxPacket[3+modbus_packege_times] = modbus_io[modbus_packege_times / 2] >> 8;
Serial_TxPacket[4+modbus_packege_times] = modbus_io[modbus_packege_times / 2];
}
// 校驗碼
CRC_check_result = calculate_crc16(Serial_TxPacket,Serial_TxPacket[2] + 3);
Serial_TxPacket[3+modbus_packege_times] = (CRC_check_result) & 0xFF;
Serial_TxPacket[4+modbus_packege_times] = (CRC_check_result>>8) & 0xFF;
Serial_SendArray(Serial_TxPacket,5+modbus_packege_times);
return ;
}
void Data_Funcion_6(void){
// Serial_TxPacket[0] = Serial_RxPacket[0]; // ID
// Serial_TxPacket[1] = Serial_RxPacket[1]; // 功能碼
modbus_io[Serial_RxPacket[3] - 1] = Serial_RxPacket[4];
modbus_io[Serial_RxPacket[3]] = Serial_RxPacket[5];
Serial_SendArray(Serial_RxPacket,Serial_RxLength);
return ;
}
void Data_Resolve(void){
// 需增加校驗位計算
modbus_check = calculate_crc16(Serial_RxPacket,Serial_RxLength-2);
if(modbus_check != 0) // 校驗是否通過
{
Serial_TxPacket[0] = 0x01; // 預設id
if(Serial_RxPacket[0] == Serial_TxPacket[0]){ // 確認id號是否一致
modbus_function = Serial_RxPacket[1];
switch(modbus_function)
{
case 3 : // 根據(jù)03功能碼,主機要求從機反饋內(nèi)容
Data_Funcion_3();
break;
case 6 :
Data_Funcion_6();
break;
// case 16 :
// Serial_SendArray(Serial_TxPacket,Serial_RxLength);
// break;
default :
break;
}
}
}
Serial_RxFlag = 0;
Serial_RxLength = 0;
}
三.使用軟件
????????使用Keil給板子上程序后,我這邊使用了Modbus Pull和Modbus Slave做實驗,網(wǎng)上可以查到并且下載。
????????我們?nèi)绻褂冒遄赢斪鲝臋C的話,那么只需要使用到Pull就可以了,不需要使用到Slave。
?????????工程代碼:
github上的項目工程文章來源:http://www.zghlxwxcb.cn/news/detail-779626.html
? ? ? ? 目前我只做了這兩個功能碼,如果這邊有什么錯誤的地方還請大佬們給出指點(????)~文章來源地址http://www.zghlxwxcb.cn/news/detail-779626.html
到了這里,關于STM32硬件庫(非HAL庫)實現(xiàn)MODBUS RTU協(xié)議的03,06功能碼(讀以及與單個發(fā)送)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!