Mode bus

原帖出处优快云博客:
http://blog.sina.com.cn/s/blog_7880d3350101liev.html

写的特别好,摘录如下:

本文参考http://blog.youkuaiyun.com/daniellee_ustb/article/details/8721883

这位博主文章,挑选出我认为比较主要的东西,写的很不错..同时在网上也搜集了这方面的资料写了一个测试代码,大家可以在看代码的同时了解整个过程.这样我想更容易理解.代码已经测试过,可以用的.http://download.youkuaiyun.com/detail/skill_cheng/5414397

 

       首先下载最新的modbus源码,所谓技术更新换代的比较快,用就用最新的东西,协议嘛也要下载最新的,google一下,在http://www.freemodbus.org/index.php?idx=5下载最新的版本freemodbus-v1.5,下载最新的协议不仅可以防止被人改动导致自己做无用功,保持原生态也可以很好的与制定者进行交流。       

       解压freemodbus-v1.5,目录结构很清晰,主要有四个文件件,分别是demo、modbus、tools、doc。其中tools为上位机测试modbus程序,doc为一些说明文件先不讨论。有用的是demo以及modbus。打开demo,没有看到stm32的工程文件,有一个叫BARE的文件夹,是一些不包括任何处理器的部分源代码,我们就用这个建立工程文件。

一、对于时钟的移植

        由于modbus RTU模式需要定时器的支持,所以第一步先移植与定时器相关的函数。

在porttimer.c中添加BOOL xMBPortTimersInit( USHORT usTim1Timerout50us )的实现,实现50us的基时时钟。

添加打开和关闭时钟的函数void vMBPortTimersEnable(  )以及voidvMBPortTimersDisable(  ),还有超时中断函数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),在其中调用上述两个发送和接收的函数。

        三、第一个功能单元——读写寄存的支持

FreeModbus源码包中BARE工程文件的main.c中定义了

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[1] =GPIO_ReadInputData(GPIOB);

        usRegInputBuf[2] =GPIO_ReadInputData(GPIOC);

        usRegInputBuf[3] =GPIO_ReadInputData(GPIOD);

        usRegInputBuf[4] =GPIO_ReadInputData(GPIOE);

        usRegInputBuf[5] =GPIO_ReadInputData(GPIOF);

        usRegInputBuf[6] =GPIO_ReadInputData(GPIOG);

        这部分定义好,我们看一下freemodbus运行的流程。

       四、freemodbus运行的软件仿真分析

       在main函数中可以看到

       int main( void ){

   eMBErrorCode    eStatus;

   eStatus = eMBInit( MB_RTU, 0x0A, 0, 38400, MB_PAR_EVEN );

        

   eStatus = eMBEnable(  );

   for( ;; ){

           ( void )eMBPoll(  );

           

           usRegInputBuf[0]++;

   }

    }

      (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 )用来检测协议栈状态用于处理消息。

五,编译后进行软件仿真查看协议栈具体运行流程

(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为接收状态机,如下图所示:


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为发送状态机,如下图所示:


 

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支持以下功能码:


 

Modbus支持的功能码

      如果检测到该帧数据是协议栈支持的功能码,就调用相应的函数进行处理,比如说Read input register,就会调用在mb.c中定义的static xMBFunctionHandler xFuncHandlers[MB_FUNC_HANDLERS_MAX]这个二维数组中注册的eMBFuncReadInputRegister函数进行处理。还记得我们之前实现的eMBRegInputCB函数有什么区别呢,为什么调用的不是这个函数呢,其实仔细看下eMBFuncReadInputRegister这个函数就行了,原来eMBFuncReadInputRegister调用了eMBRegInputCB这个函数!还以为之前搞错了。

 

    看这个函数之前先看主机发送过来的代码格式:

查询输入寄存器命令格式

    发送的数据帧里包括从机地址、功能码,寄存器起始地址、以及读寄存器的长度、CRC校验。eMBFuncReadInputRegister要做的事儿就是读出寄存器地址以及寄存器长度后调用eMBRegInputCB,读取成功后返回一个MB_ENOERR状态,表明没有错误发生。

   (3)接着看eMBPoll中的case EV_EXECUTE:执行完

    eException= xFuncHandlers[i].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[i].pxHandler(eMBRegInputCB)对接收的数据进行处理—> peMBFrameSendCur—>eMBRTUSend(&STATE_RX_IDLE)—>STATE_TX_XMIT

   串口发送完成中断—> prvvUARTTxReadyISR—> FSMpxMBFrameCBTransmitterEmpty—>xMBRTUTransmitFSM(& STATE_TX_XMIT)—>xMBPortSerialPutByte—>发送数据。

    这样是不是明白了很多,freemodbus状态机写的还是很巧妙的。

<think>我们正在解决TWI总线错误状态0x20的问题。根据引用[1]和引用[2]的内容,我们知道TWI(I2C)总线的配置涉及使能设置和GPIO配置。错误状态0x20通常表示总线在传输过程中出现了问题,比如从设备无应答(NACK)或总线竞争等。解决步骤:1.检查硬件连接:确保SCL和SDA线连接正确,上拉电阻合适(通常4.7kΩ),并且没有短路或断路。2.检查设备地址:确认从设备地址正确,且与主设备发送的地址匹配(包括读写位)。3.检查时序配置:TWI总线有时序要求,确保主设备的时钟频率在从设备支持的范围内。可以尝试降低时钟频率。4.检查总线竞争:是否有多个主设备试图同时控制总线?确保同一时间只有一个主设备。5.检查从设备状态:从设备是否就绪?是否被复位或处于忙状态?6.检查电源:确保从设备电源稳定,电压在正常范围内。根据引用[1]中的配置参数,检查twi0_used是否设置为1(启用),以及twi0_scl和twi0_sda的GPIO配置是否正确。另外,错误状态0x20在I2C协议中通常对应的是“SLA+W transmitted,NACKreceived”,即主设备发送了写地址(SLA+W)后,从设备没有应答(NACK)。因此,重点检查从设备地址是否正确以及从设备是否正常工作。如果以上步骤都无法解决问题,可以尝试使用逻辑分析仪或示波器观察TWI总线的实际波形,以确定问题所在。注意:在Linux系统中,TWI/I2C驱动可能已经提供了一些调试工具,比如通过sysfs查看设备,或者使用i2c-tools(如i2cdetect,i2cget,i2cset)进行调试。示例:使用i2cdetect扫描设备```bashi2cdetect-y0#假设总线0```如果没有检测到设备,则可能是硬件连接或地址问题。如果问题仍然存在,请检查驱动配置和内核日志(dmesg)以获取更多错误信息。最后,确保使用的操作系统(如Linux)中的TWI驱动配置正确,并且设备树(DeviceTree)中已正确声明TWI控制器和从设备。引用[2]提到F1C100s芯片,如果使用该芯片,请确认其数据手册中关于TWI控制器的特殊要求。相关问题:1.如何配置Linux下的I2C设备?2.I2C总线常见的错误状态有哪些?3.如何通过示波器调试I2C总线?</think>### 解决 TWI 总线错误状态 0x20 的方法 #### 1. **检查硬件连接** - **线缆与接口**:确认 SCL/SDA 线连接正确,无松动或短路。使用万用表检测线路通断。 - **上拉电阻**:确保 SCL/SDA 线有合适的上拉电阻(通常 4.7kΩ),电压符合设备要求(如 3.3V 或 1.8V)[^1]。 - **信号干扰**:检查是否有电磁干扰源,必要时加屏蔽或缩短线缆。 #### 2. **验证配置参数** 根据引用[1],检查 TWI 配置: - **使能状态**:确认 `twi0_used` 已设为 `1`(启用 TWI 端口)。 - **GPIO 映射**:核对 `twi0_scl` 和 `twi0_sda` 的 GPIO 引脚是否与硬件设计一致,避免冲突。 - **时钟频率**:降低 TWI 时钟速度(如从 400kHz 降至 100kHz),排除时序问题。 #### 3. **软件调试** - **错误码分析**:状态 `0x20` 通常表示 **总线仲裁失败**(多主设备竞争)或 **从设备无响应**。 - **从设备检查**: - 确认从设备地址正确(参考数据手册)。 - 检查从设备是否上电且未处于复位状态。 - **驱动日志**:在 Linux 系统使用 `dmesg` 查看内核日志,过滤 TWI/I2C 相关错误: ```bash dmesg | grep -i "i2c\|twi" ``` #### 4. **协议级排查** - **总线竞争**:若系统有多个主设备(如 CPU 和协处理器),需实现仲裁逻辑或改为单主模式。 - **信号完整性**:用示波器捕获 SCL/SDA 波形,检查: - 起始/停止信号是否完整 - 是否有毛刺或电压跌落 - ACK/NACK 响应是否正常 #### 5. **代码修复示例** 若使用裸机开发(如 F1C100s 平台),在初始化代码中需: ```c // 正确配置 GPIO 复用功能(以 F1C100s 为例) i2c_init(I2C0, 100000); // 初始化 I2C0,频率 100kHz set_gpio_function(TWI0_SCL_PIN, GPIO_FUNCTION_ALT2); // SCL 引脚复用为 ALT2 set_gpio_function(TWI0_SDA_PIN, GPIO_FUNCTION_ALT2); // SDA 引脚复用为 ALT2 ``` > **注意**:引脚复用模式需参考芯片手册(如引用[2]的 F1C100s Datasheet)。 #### 6. **系统级检查** - **电源管理**:禁用 CPU 休眠模式(如 `echo 0 > /sys/power/state`),排除休眠导致总线异常。 - **设备树配置**:在 Linux 设备树中确认 TWI 节点无冲突: ```dts &i2c0 { status = "okay"; clock-frequency = <100000>; pinctrl-names = "default"; pinctrl-0 = <&twi0_pins>; // 匹配正确的 pinctrl }; ``` ### 常见解决方案总结 | 问题类型 | 解决措施 | |------------------|-----------------------------------| | 硬件连接故障 | 重接线缆,检测上拉电阻 | | 从设备无响应 | 核对地址,检查从设备电源/复位信号 | | 总线仲裁失败 | 改为单主模式或增加重试机制 | | 驱动配置错误 | 验证 GPIO 复用和时钟频率 | ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值