/* USER CODE END TIM6_MspDeInit 1 */
}
}
还有,在stm32f0xx_it.c里要添加的中断处理函数
/* Includes ------------------------------------------------------------------*/
#include "stm32f0xx_hal.h"
#include "stm32f0xx.h"
#include "stm32f0xx_it.h"
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/* External variables --------------------------------------------------------*/
/**
* @brief This function handles TIM6 global and DAC channel underrun error interrupts.
*/
void TIM6_DAC_IRQHandler(void)
{
/* USER CODE BEGIN TIM6_DAC_IRQn 0 */
HAL_NVIC_ClearPendingIRQ(TIM6_DAC_IRQn);
/* USER CODE END TIM6_DAC_IRQn 0 */
HAL_TIM_IRQHandler(&htim6);
/* USER CODE BEGIN TIM6_DAC_IRQn 1 */
/* USER CODE END TIM6_DAC_IRQn 1 */
}
/**
* @brief This function handles System tick timer.
*/
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
HAL_SYSTICK_IRQHandler();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
/**
* @brief This function handles USART1 global interrupt (combined with EXTI line 25).
*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
if(__HAL_UART_GET_IT_SOURCE(&huart1,UART_IT_RXNE)!=RESET)
{
prvvUARTRxISR();//接收中断
}
if(__HAL_UART_GET_IT_SOURCE(&huart1,UART_IT_TXE)!=RESET)
{
prvvUARTTxReadyISR();//发送完成中断
}
HAL_NVIC_ClearPendingIRQ(USART1_IRQn);
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
串口处理的回调函数
/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* NOTE : This function Should not be modified, when the callback is needed,
the __HAL_TIM_PeriodElapsedCallback could be implemented in the user file
*/
prvvTIMERExpiredISR( );
}
现在已经有啦,谢谢哦
多谢了,我也是初次使用m0的cpu,我会把学习的点点滴滴记录下来,共同学学习
多谢了,我也是初次使用m0的cpu,我会把学习的点点滴滴记录下来,共同学学习,共同进步,加油
好的,有问题可以问我。
今天发现个问题,不知道是不是我的编译器安装的问题,怎么不能使用软件仿真呢,logic analyzer就是输不进去porta。。。呢
哎,原来是不支持啊
使用cube产生的库是HAL类,封装的感觉好不舒服,今天终于找到了这个库的说明文档,赶紧与大家分享:
Description of STM32F0xx HAL drivers
http://www.st.com/st-web-ui/static/active/en/resource/technical/document/user_manual/DM00122015.pdf
今天终于完成通信了
stm32f072 nucleo 使用内部时钟,
使用定时器基本tim6,定时时间为50us,因为我使用串口USart1的波特率为115200
usart.c
/* Includes ------------------------------------------------------------------*/
#include "usart.h"
#include "gpio.h"
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
UART_HandleTypeDef huart1;
/* USART1 init function */
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_8;
huart1.Init.OneBitSampling = UART_ONEBIT_SAMPLING_DISABLED ;
huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
HAL_UART_Init(&huart1);
}
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
GPIO_InitTypeDef GPIO_InitStruct;
if(huart->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE END USART1_MspInit 0 */
/* Peripheral clock enable */
__USART1_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
GPIO_InitStruct.Alternate = GPIO_AF1_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* Peripheral interrupt init*/
HAL_NVIC_SetPriority(USART1_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* USER CODE BEGIN USART1_MspInit 1 */
/* USER CODE END USART1_MspInit 1 */
}
}
void HAL_UART_MspDeInit(UART_HandleTypeDef* huart)
{
if(huart->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspDeInit 0 */
/* USER CODE END USART1_MspDeInit 0 */
/* Peripheral clock disable */
__USART1_CLK_DISABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9|GPIO_PIN_10);
/* Peripheral interrupt Deinit*/
HAL_NVIC_DisableIRQ(USART1_IRQn);
/* USER CODE BEGIN USART1_MspDeInit 1 */
/* USER CODE END USART1_MspDeInit 1 */
}
}
tim.c
/* Includes ------------------------------------------------------------------*/
#include "tim.h"
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
TIM_HandleTypeDef htim6;
/* TIM6 init function */
void MX_TIM6_Init(void)
{
TIM_MasterConfigTypeDef sMasterConfig;
TIM_ClockConfigTypeDef sClockSourceConfig;
uint16_t uwPrescalerValue = 0;
/* Initialize TIMx peripheral as follows:
+ Period = 35
+ Prescaler = (SystemCoreClock/20000) - 1
+ ClockDivision = 0
+ Counter direction = Up
*/
/* Compute the prescaler value to have TIMx counter clock equal to 20kHz */
uwPrescalerValue = SystemCoreClock/20000-1;//定时时基50us
htim6.Instance = TIM6;
htim6.Init.Prescaler = uwPrescalerValue;
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 35;
HAL_TIM_Base_Init(&htim6);
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;//使用内部时钟,当初我使用外部时钟,出错了,因为这板子上没有外部晶振。
HAL_TIM_ConfigClockSource(&htim6, &sClockSourceConfig);
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig);
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base)
{
if(htim_base->Instance==TIM6)
{
/* USER CODE BEGIN TIM6_MspInit 0 */
/* USER CODE END TIM6_MspInit 0 */
/* Peripheral clock enable */
__TIM6_CLK_ENABLE();
/* Peripheral interrupt init*/
HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
/* USER CODE BEGIN TIM6_MspInit 1 */
/* USER CODE END TIM6_MspInit 1 */
}
}
void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* htim_base)
{
if(htim_base->Instance==TIM6)
{
/* USER CODE BEGIN TIM6_MspDeInit 0 */
/* USER CODE END TIM6_MspDeInit 0 */
/* Peripheral clock disable */
__TIM6_CLK_DISABLE();
/* Peripheral interrupt Deinit*/
HAL_NVIC_DisableIRQ(TIM6_DAC_IRQn);
/* USER CODE BEGIN TIM6_MspDeInit 1 */
/* USER CODE END TIM6_MspDeInit 1 */
}
}
还有,在stm32f0xx_it.c里要添加的中断处理函数
/* Includes ------------------------------------------------------------------*/
#include "stm32f0xx_hal.h"
#include "stm32f0xx.h"
#include "stm32f0xx_it.h"
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/* External variables --------------------------------------------------------*/
extern TIM_HandleTypeDef htim6;
extern UART_HandleTypeDef huart1;
extern void prvvUARTTxReadyISR(void);
extern void prvvUARTRxISR(void);
extern void prvvTIMERExpiredISR( void );
/******************************************************************************/
/* Cortex-M0 Processor Interruption and Exception Handlers */
/******************************************************************************/
/**
* @brief This function handles TIM6 global and DAC channel underrun error interrupts.
*/
void TIM6_DAC_IRQHandler(void)
{
/* USER CODE BEGIN TIM6_DAC_IRQn 0 */
HAL_NVIC_ClearPendingIRQ(TIM6_DAC_IRQn);
/* USER CODE END TIM6_DAC_IRQn 0 */
HAL_TIM_IRQHandler(&htim6);
/* USER CODE BEGIN TIM6_DAC_IRQn 1 */
/* USER CODE END TIM6_DAC_IRQn 1 */
}
/**
* @brief This function handles System tick timer.
*/
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
HAL_SYSTICK_IRQHandler();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
/**
* @brief This function handles USART1 global interrupt (combined with EXTI line 25).
*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
if(__HAL_UART_GET_IT_SOURCE(&huart1,UART_IT_RXNE)!=RESET)
{
prvvUARTRxISR();//接收中断
}
if(__HAL_UART_GET_IT_SOURCE(&huart1,UART_IT_TXE)!=RESET)
{
prvvUARTTxReadyISR();//发送完成中断
}
HAL_NVIC_ClearPendingIRQ(USART1_IRQn);
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
串口处理的回调函数
/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* NOTE : This function Should not be modified, when the callback is needed,
the __HAL_TIM_PeriodElapsedCallback could be implemented in the user file
*/
prvvTIMERExpiredISR( );
}
一楼有可以通信的程序源码包,如果需要可以下载,今天只是简单的移植,内容将持续更新。
网上一片不错的freemodbus的移植详细步骤,网址http://www.tuicool.com/articles/iQF7ba
记录一下移植的分析:使用RTU模式,需要移植串口和定时器
一、对于时钟的移植
首先移植与定时器相关的函数。
在porttimer.c中添加BOOL xMBPortTimersInit( USHORT usTim1Timerout50us ),添加50us时基时钟的调用,添加时钟的开关的函数 void vMBPortTimersEnablle()和void vMBPortTimersDisable,超时中断函数void TIM6_DAC_IRQHandler(void)。
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
TIM_MasterConfigTypeDef sMasterConfig;
TIM_ClockConfigTypeDef sClockSourceConfig;
uint16_t uwPrescalerValue = 0;
/* Initialize TIMx peripheral as follows:
+ Period = 35
+ Prescaler = (SystemCoreClock/20000) - 1
+ ClockDivision = 0
+ Counter direction = Up
*/
/* Compute the prescaler value to have TIMx counter clock equal to 20kHz */
uwPrescalerValue = SystemCoreClock/20000-1;
htim6.Instance = TIM6; //使用定时器tim6
htim6.Init.Prescaler = uwPrescalerValue; //PSC=SystemCoreClock/20000-1,也就是说定时器是20KHz
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = usTim1Timerout50us; //我采用的是115200,所以这里的时间是固定的,否则为T3.5
HAL_TIM_Base_Init(&htim6);
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
HAL_TIM_ConfigClockSource(&htim6, &sClockSourceConfig);
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig);
return TRUE;
}
inline void
vMBPortTimersEnable( )
{
/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
__HAL_TIM_CLEAR_IT(&htim6,TIM_IT_UPDATE); //清除中断位
__HAL_TIM_ENABLE_IT(&htim6,TIM_IT_UPDATE); //使能中断位
__HAL_TIM_SetCounter(&htim6,0); //设置定时器计数为0
__HAL_TIM_ENABLE(&htim6); //使能定时器
}
inline void
vMBPortTimersDisable( )
{
/* Disable any pending timers. */
__HAL_TIM_DISABLE(&htim6);
__HAL_TIM_SetCounter(&htim6,0);
__HAL_TIM_DISABLE_IT(&htim6,TIM_IT_UPDATE);
__HAL_TIM_CLEAR_IT(&htim6,TIM_IT_UPDATE);
}
void TIM6_DAC_IRQHandler(void)
{
/* USER CODE BEGIN TIM6_DAC_IRQn 0 */
HAL_NVIC_ClearPendingIRQ(TIM6_DAC_IRQn);//清除中断申请位
/* USER CODE END TIM6_DAC_IRQn 0 */
HAL_TIM_IRQHandler(&htim6); //进入中断处理函数
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//定时器中断处理回调函数
{
prvvTIMERExpiredISR( );
}
二、串口的移植
使用串口usart1,有几个函数需要添加
串口的使能
UART_HandleTypeDef huart1;
BOOL xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
huart1.Instance = USART1;
huart1.Init.BaudRate = ulBaudRate;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = eParity;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_8;
huart1.Init.OneBitSampling = UART_ONEBIT_SAMPLING_DISABLED ;
huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
HAL_UART_Init(&huart1);
return TRUE ;
}
收发中断使能函数
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
/* If xRXEnable enable serial receive interrupts. If xTxENable enable
* transmitter empty interrupts.
*/
if (xRxEnable)
{
__HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);
}
else
{
__HAL_UART_DISABLE_IT(&huart1,UART_IT_RXNE);
}
if (xTxEnable)
{
__HAL_UART_ENABLE_IT(&huart1,UART_IT_TXE);
}
else
{
__HAL_UART_DISABLE_IT(&huart1,UART_IT_TXE);
}
}
发送接收函数
BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
if(HAL_UART_Transmit (&huart1 ,(uint8_t *)&ucByte,1,0x01) != HAL_OK )
return FALSE ;
else
return TRUE;
}
BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
if(HAL_UART_Receive (&huart1 ,(uint8_t *)pucByte,1,0x01) != HAL_OK )
return FALSE ;
else
return TRUE;
}
中断处理函数
void USART1_IRQHandler(void)
{
if(__HAL_UART_GET_IT_SOURCE(&huart1,UART_IT_RXNE)!=RESET)
{
prvvUARTRxISR(); //接收中断
}
if(__HAL_UART_GET_IT_SOURCE(&huart1,UART_IT_TXE)!=RESET)
{
prvvUARTTxReadyISR(); //发送中断
}
HAL_NVIC_ClearPendingIRQ(USART1_IRQn);
HAL_UART_IRQHandler(&huart1);
}
ç°å¨çåè½æ¯03
STM32F072RB Nucleo上移植freemodbus
一、对于时钟的移植
由于modbus RTU模式需要定时器的支持,所以第一步先移植与定时器相关的函数。
在porttimer.c中添加 BOOL xMBPortTimersInit ( USHORT usTim1Timerout50us )的实现,实现50us的基时时钟。
添加打开和关闭时钟的函数 void vMBPortTimersEnable( ) 以及 void vMBPortTimersDisable( ) ,还有超时中断函数 voidTIM4_IRQHandler(void) 。
二、对串口通信的移植
无论是modbus ASCII还是RTU模式,都以串口通讯做为载体,需要添加串口的使能 BOOL xMBPortSerialInit ,收发中断的使能 voidvMBPortSerialEnable( BOOLxRxEnable, BOOL xTxEnable ) ,发送以及接收 BOOL xMBPortSerialPutByte( CHAR ucByte ) ,BOOL xMBPortSerialGetByte( CHAR * pucByte ),这几个函数没什么好说的,有两个中断函数我比较好奇,就是static void prvvUARTTxReadyISR( void )以及staticvoid prvvUARTRxISR( void ),就是一个发送中断一个接收中断,为什么是这样的名字呢,stm32串口发生中断怎么去调用它们呢,如果换成其他单片机,为什么是这样的一个名字呢?原来在freemodbus中并没有提供中断函数的具体名称,还需要根据自己使用的处理器自己添加中断处理函数 voidUSART1_IRQHandler(void) ,在其中调用上述两个发送和接收的函数。
三、第一个功能单元——读写寄存的支持
eMBErrorCode eMBRegInputCB() 、eMBErrorCode eMBRegHoldingCB() 、eMBErrorCode eMBRegCoilsCB()以及eMBErrorCode eMBRegDiscreteCB()四个函数与从机寄存器做了接口,并给出了eMBErrorCode eMBRegInputCB()的例子,在工程中建立portreg.c来实现这几个函数。
其中eMBErrorCode eMBRegInputCB()为读寄存器的值,eMBErrorCode eMBRegHoldingCB()为向单片机写入寄存器的值,eMBErrorCodeeMBRegCoilsCB()为读写多个开关量的函数,eMBErrorCodeeMBRegDiscreteCB()为读多个离散开关量的函数。
为了方便测试,我们只先实现第一个eMBErrorCodeeMBRegInputCB()读连续多个寄存器值的函数,比如实现读GPIOA-GPIOG的值。
#define REG_INPUT_START 0
#define REG_INPUT_NREGS 7
在eMBErrorCode eMBRegInputCB()开始部分加入度寄存器值语句,以备查询时使用。
比如参数UCHAR *pucRegBuffer, USHORT usAddress = 0, USHORT usNRegs = 3 就是读取端口GPIOA-GPIOC的值
usRegInputBuf[0] =GPIO_ReadInputData(GPIOA);
usRegInputBuf[6] =GPIO_ReadInputData(GPIOG);
这部分定义好,我们看一下freemodbus运行的流程。
四、freemodbus 运行的软件仿真分析
在main函数中可以看到
int main( void ) {
/* Enable the Modbus Protocol Stack. */
{
(1)其中调用了
eMBErrorCode eMBInit( eMBMode eMode, UCHARucSlaveAddress, UCHAR ucPort, ULONGulBaudRate, eMBParity eParity );
其目的就是选择要使用的模式是RTU、ASCII码还是TCP方式,如果选择的模式是RTU或ASCII模式其他都是串口的一些设置;如果选择的是TCP模式,需要调用到eMBErrorCode eMBTCPInit( USHORT usTCPPort )只需要制定端口号即可。这里我们先用RTU模式做测试。在这同时也对定时器进行了初始化。
(2)之后调用了
eMBErrorCode eMBEnable( void );来使能modbus协议栈,其中调用pvMBFrameStartCur( ),在eMBInit根据模式选择的不同,pvMBFrameStartCur( )会有不同的原型,这里选用的是RTU模式,那么将调用eMBRTUStart,其中调用了 vMBPortSerialEnable vMBPortTimersEnable来时能串口和定时器,使能了超时定时器,故经过T35时间后,发生第一次超时中断,在中断中,向协议栈发送消息EV_READY(Startupfinished),并调用voidvMBPortTimersDisable( )关闭超时定时器,同时将eRcvState设为STATE_RX_IDLE。此时,协议栈可以接收串口数据。
(3)最后调用
eMBErrorCode eMBPoll( void )用来检测协议栈状态用于处理消息。
五,编译后进行软件仿真查看协议栈具体运行流程
打开keil软件仿真器,软件仿真进行单步运行,如下图1所示。对工程进行软件仿真
(1)执行main()中eStatus = eMBInit( MB_RTU, 0x0A, 0, 9600,MB_PAR_NONE );,我们进去看一下,可以看到,我们初始化的slave address是0x0A,modbus支持1-247个从地址。我们制定的是RTU模式,所以要对RTU一些参数进行赋值,如所调用的初始化函数为
pvMBFrameStartCur = eMBRTUStart;
pvMBFrameStopCur = eMBRTUStop;
peMBFrameSendCur = eMBRTUSend;
peMBFrameReceiveCur =eMBRTUReceive;
pvMBFrameCloseCur =MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;
pxMBFrameCBByteReceived =xMBRTUReceiveFSM;
pxMBFrameCBTransmitterEmpty =xMBRTUTransmitFSM;
pxMBPortCBTimerExpired =xMBRTUTimerT35Expired;
其中包括了modbus的启动、停止、发送以及接收数据的函数具体形式,其中比较关心的是xMBRTUReceiveFSM、xMBRTUTransmitFSM这两个函数,规定了modbus状态转换的状态机,其中xMBRTUReceiveFSM为接收状态机,如下图2所示:
xMBRTUReceiveFSM 实现的 modbus 接收状态机
可以这样描述以上状态转换状态,上电启动或复位进入STATE_RX_INIT状态,为了防止协议栈在初始化过程中就收到串口数据,要放弃这个无效的数据,要先等待一个T35时间,过了这个时间才进入STATE_RX_IDLE状态,开始接收数据。实际上只要注意系统启动顺序,这个问题还是可以避免的。进入STATE_RX_IDLE状态后,从收到第一个字符数据开始启动定时器进入STATE_RX_RCV状态,每接收一个字节都要重新复位定时器开始定时,直到出现一个T35超时。如果接收的数据符合规定的数据格式,那么整帧数据接收完毕,又回到STATE_RX_IDLE状态,如果数据过长那么就到STATE_RX_ERROR状态,等待整帧接收完毕,放弃整个帧然后进入STATE_RX_IDLE状态。当然实现所有状态的状换还需要BOOL xMBRTUTimerT35Expired(void )函数。
其中xMBRTUTransmitFSM为发送状态机,如下图3所示:xMBRTUTransmitFSM实现的modbus发送状态机
说到发送状态机就简单多了,因为不用去判断自己发送的时候是否会超时,基本上就是没需求就在STATE_TX_IDLE状态,要发送就进入STATE_TX_XMIT状态。
看到xMBRTUTimerT35Expired这里想到一个问题,在timer初始化的时候设定的时基是50us的时间就中断一次,那么modbus是如何实现不同波特率下3.5个字符时间判断的呢?我们看到在timer中断中调用了pxMBPortCBTimerExpired这个函数,但是实际上T35超时函数调用的是xMBRTUTimerT35Expired这个函数,它们之间是什么关系呢?发现在eMBRTUInit中对xMBPortTimersInit( ( USHORT ) usTimerT35_50us)进行了重新初始化,如果波特率大于19200,那么就采用固定的1800us时间,否则就用3.5个字符时间间隔。我觉得之所以在波特率大的时候才用固定的时间可能为了防止出现判断时间过小时误判了正常的线路延迟的原因。
(2)运行到eStatus = eMBEnable( )这里。这个比较简单,就是激活协议栈以及串口和定时器而已。
(3)运行到( void )eMBPoll( ),进去看一下。这里面比较关键的地方就是if( xMBPortEventGet( &eEvent ) == TRUE )控制了一个事件处理状态机,这就要问了,这个事件处理是个啥玩意?也许进去看看就可以知道了。原来在这个获取事件的玩意还有个叫事件队列的东西*eEvent = eQueuedEvent,原来这是上述两个状态机和一个超时函数中会反馈一些事件例如xMBPortEventPost( EV_FRAME_SENT ),来供调度。具体调度方法不去深究。
六、对modbus RTU模式的接收发送机制分析
明白了RTU模式的运行流程,下面对RTU模式的接收和发送模式的分析。
(1)我们用于使用的接收buf是哪个呢?什么状态时数据是有效的?
对于这个问题,我们还得从xMBRTUReceiveFSM入手,可以发现当进入STATE_RX_IDLE状态时可能是一帧数据传输完毕,到mbrtu.c 中的eMBInit里看一下,显然STATE_RX_IDLE接收了第一个字节后,STATE_RX_RCV将剩下的字节放到了ucRTUBuf中,usRcvBufferPos为接收的数据长度,那么肯定应该在经过一个T35的时间后结束的对不对,去看看就知道了。在xMBRTUTimerT35Expired函数中发现了以下语句:
case STATE_RX_RCV:
xNeedPoll = xMBPortEventPost(EV_FRAME_RECEIVED );
break;
不就说明当eMBPoll 收到EV_FRAME_RECEIVED消息的时候,告诉eMBPoll新的一帧数据来了,要去ucRTUBuf中对usRcvBufferPos的数据进行处理吗,这就对了!我们在返回到eMBPoll这个函数中一探究竟。
现在再看事件探测器if( xMBPortEventGet( &eEvent ) == TRUE ) 就明朗多了,当触发case EV_FRAME_RECEIVED:时,调用peMBFrameReceiveCur去获取消息帧,这个函数的实现是谁呢?就是eMBRTUReceive!打开一看就一目了然。eMBRTUReceive把ucRTUBuf的数据信息取出来通过eStatus =peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength ); 重新赋给了从机地址ucRcvAddress、帧内容开始地址ucMBFrame、数据长度usLength(不包括地址位以及CRC校验位)。
(2)消息取出来了,怎么用?还是回到eMBPoll接着看case EV_FRAME_RECEIVED:接下来干啥了,检查消息是不是给我们的,要是不是广播的消息并且没有发生异常我们就进行处理,我们的地址在eMBInit中已经设定。于是就到了case EV_EXECUTE:状态,接下来就要看发来的数据帧是干什么的了,这要通过功能码来判断。
Freemodbus支持以下功能码如图4:
Modbus支持的功能码
如果检测到该帧数据是协议栈支持的功能码,就调用相应的函数进行处理,比如说Read input register,就会调用 在mb.c中定义的static xMBFunctionHandler xFuncHandlers[MB_FUNC_HANDLERS_MAX]这个二维数组中注册的eMBFuncReadInputRegister函数进行处理。还记得我们之前实现的eMBRegInputCB函数有什么区别呢,为什么调用的不是这个函数呢,其实仔细看下eMBFuncReadInputRegister这个函数就行了,原来eMBFuncReadInputRegister调用了eMBRegInputCB这个函数!还以为之前搞错了。
看这个函数之前先看主机发送过来的代码格式如图5:
发送的数据帧里包括从机地址、功能码,寄存器起始地址、以及读寄存器的长度、CRC校验。eMBFuncReadInputRegister要做的事儿就是读出寄存器地址以及寄存器长度后调用eMBRegInputCB,读取成功后返回一个MB_ENOERR状态,表明没有错误发生。
(3)接着看eMBPoll中的case EV_EXECUTE:执行完
eException= xFuncHandlers.pxHandler( ucMBFrame, &usLength );这个处理函数,寄存器值读出来了,帧格式也写好了,什么地方调用发送函数了呢?找了大半天,终于在串口中断这个地方看到了,原来当串口 发送 完成时调用了prvvUARTTxReadyISR,又调用了pxMBFrameCBTransmitterEmpty,也就是xMBRTUTransmitFSM发送状态机,当满足STATE_TX_XMIT状态时,调用xMBPortSerialPutByte将数据发送出去。
那么是什么东西使发送状态机从STATE_TX_IDLE变化到了STATE_TX_XMIT状态呢?我们的eMBRTUSend似乎没有用到?那么到eMBRTUSend里面去看看他做了什么。原来它做的事情就是当状态机处于STATE_RX_IDLE接收空闲的时候把发送缓冲区pucSndBufferCur的数据填写好从机地址、对其中数据进行CRC16校验,把状态机置为STATE_TX_XMIT,使能串口。
到底是谁调用了eMBRTUSend呢?真是一个问题接着一个问题呀。搜一下peMBFrameSendCur这个函数,原来还是在eMBPoll中的case EV_EXECUTE:中,如果不是广播消息的话,我们就返回一条消息,于是调用了peMBFrameSendCur。
说了大半天可能都给搞糊涂了,整理下发送和接收的整体思路:
协议栈以及定时器初始化T35 第一次超时—>eMBPoll STATE_RX_IDLE—>收到数据中断—>prvvUARTRxISR—>pxMBFrameCBByteReceived—>xMBRTUReceiveFSM接收数据 —> STATE_RX_RCV—>T35超时—> eMBPollEV_FRAME_RECEIVED(peMBFrameReceiveCur-> eMBRTUReceive )提取完整数据帧—> eMBPoll case EV_EXECUTE: xFuncHandlers.pxHandler (eMBRegInputCB )对接收的数据进行处理—> peMBFrameSendCur—>eMBRTUSend(&STATE_RX_IDLE)—>STATE_TX_XMIT
串口发送完成中断—> prvvUARTTxReadyISR—> FSMpxMBFrameCBTransmitterEmpty—>xMBRTUTransmitFSM(& STATE_TX_XMIT)—>xMBPortSerialPutByte—>发送数据。
这样是不是明白了很多,freemodbus状态机写的还是很巧妙的。
七、RTU模式的测试
(1)测试软件
测试软件采用串口调试助手或modbus调试精灵V1.024,外加一个CRC校验码计算工具,如下图6所示: