Freemodbus启动流程分析

news2025/1/15 6:41:04

近项目有用到modbus协议,于是在网上找了些资料成功将freemodbus移植到m3,由于移植过程较简单,网上教程也很多,这里我们就不再赘述.我用到的freemodbus版本是V1.5,下面附上新的源码下载地址:http://www.freemodbus.org/index.php?idx=5

下面开始分析下freemodbus得启动流程,老规矩我们还是从main()函数下手:

和freemodbus有关的函数只有三个eMBInit(), eMBEnable(), eMBPoll().我们逐一来分析.

首先是eMBInit(),我们来看下源码:eMBErrorCode

eMBInit( eMBMode eMode, UCHAR ucSlaveAddress, UCHAR ucPort,

ULONG ulBaudRate, eMBParity eParity )

{

//错误状态初始值

eMBErrorCode eStatus = MB_ENOERR;

//验证从机地址

if( ( ucSlaveAddress == MB_ADDRESS_BROADCAST ) ||

( ucSlaveAddress < MB_ADDRESS_MIN ) ||

( ucSlaveAddress > MB_ADDRESS_MAX ) )

{

eStatus = MB_EINVAL;

}

else

{

ucMBAddress = ucSlaveAddress;

switch ( eMode )

{

#if MB_RTU_ENABLED > 0

case MB_RTU:

pvMBFrameStartCur = eMBRTUStart;

pvMBFrameStopCur = eMBRTUStop;

peMBFrameSendCur = eMBRTUSend;

//报文接收函数

peMBFrameReceiveCur = eMBRTUReceive;

pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;

//接收状态机

pxMBFrameCBByteReceived = xMBRTUReceiveFSM;

//发送状态机

pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM;

//报文到达间隔检查

pxMBPortCBTimerExpired = xMBRTUTimerT35Expired;

//初始化RTU

eStatus = eMBRTUInit( ucMBAddress, ucPort, ulBaudRate, eParity );

break;

#endif

#if MB_ASCII_ENABLED > 0

case MB_ASCII:

pvMBFrameStartCur = eMBASCIIStart;

pvMBFrameStopCur = eMBASCIIStop;

peMBFrameSendCur = eMBASCIISend;

peMBFrameReceiveCur = eMBASCIIReceive;

pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;

pxMBFrameCBByteReceived = xMBASCIIReceiveFSM;

pxMBFrameCBTransmitterEmpty = xMBASCIITransmitFSM;

pxMBPortCBTimerExpired = xMBASCIITimerT1SExpired;

eStatus = eMBASCIIInit( ucMBAddress, ucPort, ulBaudRate, eParity );

break;

#endif

default:

eStatus = MB_EINVAL;

}

//

if( eStatus == MB_ENOERR )

{

if( !xMBPortEventInit() )

{

/* port dependent event module initalization failed. */

eStatus = MB_EPORTERR;

}

else

{

//设定当前状态

eMBCurrentMode = eMode;

eMBState = STATE_DISABLED;

}

}

}

return eStatus;

}

我这次用到的是RTU模式,所以我们就只分析RTU模式下得工作模式.上边的代码比较容易理解, 大家好逐行分析,我们首先来看下都传了什么参数:

eMBInit(MB_RTU, 0x09, 0x01, 9600, MB_PAR_NONE);

附上函数的声明:eMBInit( eMBMode eMode, UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity )

MB_RTU:使用的是modbusRTU模式.

0x09:从机得地址

0x01:串口号,这里我们使用的是串口1

9600:波特率

MB_PAR_NONE:无校验和

这个函数的任务就是根据你的参数来配置串口,定时器和modbus中的ID号这个函数另外一个重要的工作就是将一些全局指针变量指向对应得函数,将来方便进行回调。

另外一个事情就是将eMBCurrentMode赋值为MB_RTU, eMBState 赋值为STATE_DISABLED;

我们需要对这些状态有一些印象,因为后边还是会用到。

下面来分析eMBEnable():eMBErrorCode

eMBEnable( void )

{

eMBErrorCode eStatus = MB_ENOERR;

if( eMBState == STATE_DISABLED )

{

/* Activate the protocol stack. */

pvMBFrameStartCur( );

eMBState = STATE_ENABLED;

}

else

{

eStatus = MB_EILLSTATE;

}

return eStatus;

}

这一段代码呢比较少,我们来看第6行的判断,这里eMBState 赋值为STATE_DISABLED是在

eMBInit()执行,所以执行if分支,看第9行代码,发现并没有这个函数,奥,,还记得他是函数指针吧,在eMBInit()中被指向了函数eMBRTUStart( void ),我们来看下真正的启动函数原型:void

eMBRTUStart( void )

{

ENTER_CRITICAL_SECTION( );

/* Initially the receiver is in the state STATE_RX_INIT. we start

* the timer and if no character is received within t3.5 we change

* to STATE_RX_IDLE. This makes sure that we delay startup of the

* modbus protocol stack until the bus is free.

*/

//eRcvState 初始化状态

eRcvState = STATE_RX_INIT;

//使能接收,禁止发送

vMBPortSerialEnable( TRUE, FALSE );

//启动定时器

vMBPortTimersEnable();

EXIT_CRITICAL_SECTION( );

}

以上代码比较简单我就不逐一分析了,我们需要注意下eRcvState被赋值为 STATE_RX_INIT;还将串口接收中断使能,关闭串口发送。开启定时器,记得哦,开启了定时器并开启了中断,你肯定会想定时器多久后触发中断?我们来分析一下,首先我们找到初始化定时器的地方,eMBInit()èeMBRTUInit(),我们来看一下源码:eMBErrorCode

eMBRTUInit( UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate,

eMBParity eParity )

{

eMBErrorCode eStatus = MB_ENOERR;

ULONG usTimerT35_50us;

( void )ucSlaveAddress;

ENTER_CRITICAL_SECTION();

/* Modbus RTU uses 8 Databits. */

if( xMBPortSerialInit( ucPort, ulBaudRate, 8, eParity ) != TRUE )

{

eStatus = MB_EPORTERR;

}

else

{

/* If baudrate > 19200 then we should use the fixed timer values

* t35 = 1750us. Otherwise t35 must be 3.5 times the character time.

*/

//如果波特率超过19200 使用固定的时间间隔,1750us

//其他情况,则要进行计算。

if( ulBaudRate > 19200 )

{

usTimerT35_50us = 35; /* 1750us. */

}

else

{

/* The timer reload value for a character is given by:

*

* ChTimeValue = Ticks_per_1s / ( Baudrate / 11 )

* = 11 * Ticks_per_1s / Baudrate

* = 220000 / Baudrate

* The reload for t3.5 is 1.5 times this value and similary

* for t3.5.

*/

usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );

}

//初始化定时器

if( xMBPortTimersInit( ( USHORT ) usTimerT35_50us ) != TRUE )

{

eStatus = MB_EPORTERR;

}

}

EXIT_CRITICAL_SECTION( );

return eStatus;

}

串口初始化我们就不多说了,接下来它在算出来了一个数给了usTimerT35_50us,然后调用xMBPortTimersInit( ( USHORT ) usTimerT35_50us ),这是在干嘛?

首先我们来详细说下usTimerT35_50us,这里我首先要说下modbus的协议规范了:

RTU方式的MODBUS如何判别一帧数据包哪?有的协议里有开始标示、结束标示,通过判别开头标示和结束标示来表示一帧完整的数据帧。但MODBUS RTU方式的数据帧并没有开始标示和结束标示,那MODBUS RTU怎么判别一帧数据帧的哪?我们还要看MDBUS协议栈的具体规定。先给大家看一个图:

MODBUS协议里面说一帧数据和下一帧数据之间的间隔至少是3.5个字符。好了,新的问题又来了3.5个字符是多长时间,我们来看段代码中的说明:

* If baudrate > 19200 then we should use the fixed timer values

* t35 = 1750us. Otherwise t35 must be 3.5 times the character time.

*/

波特率大于19200时候我们用固定时长来判断,小于19200时我们还是要知道3.5个字符是多久,我们知道他和波特率有关,我们假设波特率是9600,那1s能传9600位, 一个字符是11位(8个数据位,1个起始位,1个停止位, 1个校验位),那T3.5就等价于1000/(9600/11)*3.5(估算为4ms)。

Freemodbus实现t3.5的方法是在定时器设置一个基准时间50us(通过设置预分频的数值TIM_Prescaler),再根据T3.5的大小来计算出计数值(TIM_Period)。好了我们这里就点到为止,我们只需要将定时器配置基准时间为50us。

诶,,,我们刚到哪里了?

刚调用了一个钩子函数pvMBFrameStartCur( );随后将eMBState状态改编为STATE_ENABLED;

到这里我们总结一下,刚刚我们已经执行了两个函数,分别是eMBInit(MB_RTU, 0x09, 0x01, 9600, MB_PAR_NONE)和eMBEnable(); 有两个状态发生了改变,eMBState状态改变为STATE_ENABLED,eRcvState 状态改变为 STATE_RX_INIT; 并且在大约4ms后会产生一次定时器中断。

接下来我们先去看下这个定时器中断都干了什么?然后再去看eMBPoll();void TIM4_IRQHandler(void)

{

if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET)

{

//清除定时器T4溢出中断标志位

TIM_ClearITPendingBit(TIM4, TIM_IT_Update);

prvvTIMERExpiredISR( );

}

}

判断确定发生中断以后,又调用了一个prvvTIMERExpiredISR( )è pxMBPortCBTimerExpired()这又是一个钩子函数,我们来看下代码:

BOOL

xMBRTUTimerT35Expired( void )

{

BOOL xNeedPoll = FALSE;

switch ( eRcvState )

{

/* Timer t35 expired. Startup phase is finished. */

//这是一个启动状态,运行到这里说明启动状态完成。

case STATE_RX_INIT:

xNeedPoll = xMBPortEventPost( EV_READY );

break;

/* A frame was received and t35 expired.

* Notify the listener that

* a new frame was received. */

case STATE_RX_RCV:

//发送事件,接收到完整的modbus数据

xNeedPoll = xMBPortEventPost( EV_FRAME_RECEIVED );

break;

/* An error occured while receiving the frame. */

case STATE_RX_ERROR:

break;

/* Function called in an illegal state. */

default:

assert( ( eRcvState == STATE_RX_INIT ) ||

( eRcvState == STATE_RX_RCV ) ||

( eRcvState == STATE_RX_ERROR ) );

}

//禁止定时器

vMBPortTimersDisable( );

//串口接收状态 变为空闲状态。

eRcvState = STATE_RX_IDLE;

return xNeedPoll;

}

上来是根据eRcvState的值来运行对应分支的,eRcvState的值又是什么呢?我们之前说过,在eMBRTUStart()中eRcvState = STATE_RX_INIT;那我们就知道要执行哪些代码了,首先调用了 xNeedPoll = xMBPortEventPost( EV_READY ); xMBPortEventPost()就是将事件类型赋给eQueuedEvent,并将xEventInQueue置为TRUE代表有事件发生。BOOL

xMBPortEventPost( eMBEventType eEvent )

{

//有事件标志更新

xEventInQueue = TRUE;

//设定事件标志

eQueuedEvent = eEvent;

return TRUE;

}

我们继续回到xMBRTUTimerT35Expired(),他在将对应的事件发送给eQueuedEvent后关闭了定时器,并将eRcvState置为STATE_RX_IDLE

下面开始分析eMBPoll();static UCHAR *ucMBFrame;

static UCHAR ucRcvAddress;

static UCHAR ucFunctionCode;

static USHORT usLength;

static eMBException eException;

int i;

eMBErrorCode eStatus = MB_ENOERR;

eMBEventType eEvent;

/* Check if the protocol stack is ready. */

//eMBEnable()==> eMBState = STATE_ENABLED;

if( eMBState != STATE_ENABLED )

{

return MB_EILLSTATE;

}

/* Check if there is a event available.

If not return control to caller.

* Otherwise we will handle the event. */

//查询事件

if( xMBPortEventGet( &eEvent ) == TRUE )

{

switch ( eEvent )

{

case EV_READY:

break;

case EV_FRAME_RECEIVED:

//接收报文函数,传入参数从机地址,报文指针,长度

//实际上调用了eMBRTUReceive 位于mbrtu.c

eStatus = peMBFrameReceiveCur( &ucRcvAddress,

&ucMBFrame, &usLength );

if( eStatus == MB_ENOERR )

{

/* Check if the frame is for us.

If not ignore the frame. */

//验证报文从机地址

if( ( ucRcvAddress == ucMBAddress ) ||

( ucRcvAddress == MB_ADDRESS_BROADCAST ) )

{

//发送事件,报文到达,可以进行处理

( void )xMBPortEventPost( EV_EXECUTE );

}

}

break;

case EV_EXECUTE:

ucFunctionCode = ucMBFrame[MB_PDU_FUNC_OFF];

eException = MB_EX_ILLEGAL_FUNCTION;

for( i = 0; i < MB_FUNC_HANDLERS_MAX; i++ )

{

/* No more function handlers registered. Abort. */

if( xFuncHandlers[i].ucFunctionCode == 0 )

{

break;

}

else if( xFuncHandlers[i].ucFunctionCode == ucFunctionCode )

{

eException = xFuncHandlers[i].pxHandler( ucMBFrame, &usLength );

break;

}

}

/* If the request was not sent to the broadcast address we

* return a reply. */

if( ucRcvAddress != MB_ADDRESS_BROADCAST )

{

if( eException != MB_EX_NONE )

{

/* An exception occured. Build an error frame. */

usLength = 0;

ucMBFrame[usLength++] = ( UCHAR )( ucFunctionCode | MB_FUNC_ERROR );

ucMBFrame[usLength++] = eException;

}

if( ( eMBCurrentMode == MB_ASCII ) && MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS )

{

vMBPortTimersDelay( MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS );

}

eStatus = peMBFrameSendCur( ucMBAddress, ucMBFrame, usLength );

}

break;

case EV_FRAME_SENT:

break;

}

}

return MB_ENOERR;

这里代码比较多,我们逐行分析下。

13-16行检测eMBState是否为STATE_ENABLED?之前我们在eMBEnable()已经将eMBState置为STATE_ENABLED

23-89行检测是否有事件发生,并执行事件对应的处理函数。

eMBPoll()就是一直在检测是否有事件,有就处理。我们先来看xMBPortEventGet( &eEvent )BOOL

xMBPortEventGet( eMBEventType * eEvent )

{

BOOL xEventHappened = FALSE;

//若有事件更新

if( xEventInQueue )

{

//获得事件

*eEvent = eQueuedEvent;

xEventInQueue = FALSE;

xEventHappened = TRUE;

}

return xEventHappened;

}

代码比较简单,就是检测是否有事件,并将事件回传给掉它的函数。刚刚我们发生了一个事件,还记得吗?在第一次定时器中断的时候 执行了这样一句话xNeedPoll = xMBPortEventPost( EV_READY );

可惜EV_READY事件只是告诉主程序协议栈初始化成功,并没有做实际的事情。

好了,到这里我们大概将freemodbus的启动流程分析了一下,我们来总结一下。

eMBInit():配置了定时器和串口,并规定了modbus的启动、停止、发送以及接收数据的函数具体形式。

eMBEnable():打开定时器并开启中断, 使能串口接收并开启中断。

eMBPoll():检测是否有事件发生,并处理对应的事件,包括接收到完整数据帧的事件,处理数据帧的事件。

协议栈剩下的工作就是接收串口的数据并进行分析处理,并会通过串口返回对应的回应帧。我们将会在下面分析modbus RTU模式的接收发送机制分析。

嵌入式物联网需要学的东西真的非常多,千万不要学错了路线和内容,导致工资要不上去!

无偿分享大家一个资料包,

差不多150多G。里面学习内容、面经、项目都比较新也比较全!某鱼上买估计至少要好几十。(点击找小助理领取)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/110084.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android设计模式详解之解释器模式

前言 解释器模式是一种使用较少的行为型模式&#xff1b; 提供了一种解释语言的语法或表达式的方式&#xff0c;通过该接口解释一个特定的上下文。 定义&#xff1a;给定一个语言&#xff0c;定义它的文法的一种表示&#xff0c;并定义一个解释器&#xff0c;该解释器使用该表…

MySQL面试常问问题(高可用/性能 + 运维) —— 赶快收藏

1.数据库读写分离了解吗&#xff1f; 读写分离的基本原理是将数据库读写操作分散到不同的节点上&#xff0c;下面是基本架构图&#xff1a; 读写分离 读写分离的基本实现是: 数据库服务器搭建主从集群&#xff0c;一主一从、一主多从都可以。 数据库主机负责读写操作&#x…

洛谷——P1573 栈的操作

文章目录栈的操作题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1提示AC代码栈的操作 题目描述 现在有四个栈&#xff0c;其中前三个为空&#xff0c;第四个栈从栈顶到栈底分别为 1,2,3,⋯,n1,2,3,\cdots ,n1,2,3,⋯,n。每一个栈只支持一种操作&#xff1a;弹出并压入…

当云原生成为一种显学,对象存储和数据湖如何顺势而为

前言&#xff1a; 已经成为数字化时代显学的云原生并非单项技术&#xff0c;而是一种重塑了软件开发和和业务运行应用的设计思想&#xff0c;是一套技术体系和方法论。云原生“Cloud Native”的Cloud 是指云平台&#xff0c;Native则表示应用程序从设计之初即使用云环境、天生…

MyBatis学习 | SQL映射文件

文章目录一、简介二、insert、update和delete标签2.1 关于增删改2.2 获取自增主键的值三、参数处理3.1 获取不同形式的参数3.1.1 获取单个参数3.1.2 获取多个参数3.2 #{Key}3.2.1 #{}&#x1f19a;${}3.2.2 #{}中设置参数规则四、select标签4.1 select标签的主要属性4.2 关于返…

即时通讯音视频开发视频编解码理论

从信息论的观点来看&#xff0c;描述信源的数据是信息和数据冗余之和&#xff0c;即&#xff1a;数据信息数据冗余。数据冗余有许多种&#xff0c;如空间冗余、时间冗余、视觉冗余、统计冗余等。将图像作为一个信源&#xff0c;视频压缩编码的实质是减少图像中的冗余。 视频为何…

2步就能实现给视频去色并裁剪画面

看到很多小伙伴还不知道大量的视频怎么实现批量的进行去色处理&#xff0c;并且裁剪视频画面大小的方法&#xff0c;小编今天就来教大家一个可以快速操作的简单方法&#xff0c;感兴趣的朋友们快进来瞧瞧吧&#xff01; 首先我们来看看用这个方法操作剪辑出来的效果&#xff0c…

预焙阳极行业现状:供给格局边际将改善 “双碳”下优质产品迎新机遇

预焙阳极属于碳素制品&#xff0c;是电解铝生产过程中不可缺少的大宗原材料。从用途来看&#xff0c;预焙阳极仅用作电解铝过程中电解槽的阳极材料&#xff0c;既作为导体&#xff0c;又参与电化学反应而产生消耗&#xff0c;预焙阳极的品质会对原铝的质量产生重要影响。 一、预…

免费PDF阅读器有哪些? 14款强烈推荐的PDF阅读器!

即使经过这么多年&#xff0c;PDF 仍然是最受欢迎的阅读格式之一。从阅读电子书或填写在线表格到创建用户手册&#xff0c;PF 格式仍然是最受欢迎的阅读方式。虽然现在的网络浏览器已经配备了基本的 PDF 阅读功能&#xff0c;但您仍然需要单独下载 PDF 阅读器才能实现填写表格、…

「另类」图达通,还缺一个二次进化

作者 | 张祥威 编辑 | 于婷中国的激光雷达公司早期都很幸运&#xff0c;禾赛、速腾聚创和图达通三家&#xff0c;分别遇到了自己的伯乐——蔚小理。 比较特别的是图达通&#xff0c;它与蔚来的合作之紧密&#xff0c;程度远超另外两家&#xff0c;堪称命中贵人。 根据图达通联合…

p5.js 光速入门

本文简介 点赞 关注 收藏 学会了 本文的目标是和各位工友一起有序的快速上手 p5.js &#xff0c;会讲解 p5.js 的基础用法。 本文会涉及到的内容包括&#xff1a; 项目搭建p5.js 基础2D图形文字图形样式设置图片事件&#xff08;交互相关的&#xff09;基础动画 其中还会…

Ubuntu四轮小车仿真教程gazebo

主要实现内容为在ROS环境下基于Gazebo仿真软件创建一个四轮小车&#xff0c;并实现小车的控制&#xff0c;如下图所示&#xff0c;接下来教程将会进行详细解释。 1.创建工作空间 创建ROS工作空间&#xff0c;命名为SmartCar&#xff0c;并在该工作空间中创建src文件夹。 mkdi…

数字三渔冲:打造美丽乡村新范式

年初&#xff0c;中共中央 国务院关于做好 2022 年全面推进乡村振兴重点工作的意见中提到&#xff0c;要大力推进数字乡村建设&#xff0c;以数字技术赋能乡村公共服务。沿着乡村振兴的战略导向&#xff0c;并紧随筑堡工程共同缔造号召&#xff0c;长阳三渔冲村引入了 SENSORO …

[ Linux ] 死锁以及如何避免死锁

目录 1.什么是死锁&#xff1f; 死锁 2.模拟死锁情况 3.死锁四个必要条件 4.避免死锁的方法 5.避免死锁的算法 银行家算法&#xff08;了解为主&#xff09; 1.什么是死锁&#xff1f; 死锁 死锁是指在一组进程中的各个进程均占有不会释放的资源&#xff0c;但因互相申…

Android入门第54天-SQLite中的Transaction

简介 上一篇我们完整的介绍了SQLite在Android中如何使用&#xff0c;今天我们要来讲一下“Transaction“即事务这个问题。 我们经常在编程中会碰到这样的业务场景&#xff1a; 没问题一系列有业务关联性表操作的数据一起提交&#xff1b;事务中只要有一步有问题&#xff0c;那…

PCL 点云最小生成树(MST,Dijkstra算法)

文章目录 一、简介二、实现代码三、实现效果参考文献一、简介 之前使用过Kruskal算法创建过最小生成树(Open3D 点云最小生成树算法(MST,Kruskal算法)),这里使用另一种算法(Dijkstra算法)来实现创建一个最小生成树,原始的Dijkstra算法并不适用于去生成最小生成树,因此…

xxe-lab靶场安装和简单php代码审计

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是xxe-lab靶场安装和简单php代码审计。 一、xxe-lab靶场简介 xxe-lab是一个使用java、python、php和C#四种编程语言开发的存在xxe漏洞的web小型靶场。利用该靶场可以简单研究xxe漏洞&#xff0c;并且对于这四种编…

Win10微软输入法打不出汉字?

在Win10系统中自带的微软输入法无需再安装其他拼音输入法就可以轻松输入汉字&#xff0c;非常方便&#xff0c;但是有的用户却遇到了Win10专业版自带的微软输入法打不出汉字的问题&#xff0c;这要如何解决呢&#xff1f;有需要的用户就来一起看看吧。 1、点击系统左下侧的wind…

Allegro如何更改铜皮的网络操作指导

Allegro如何更改铜皮的网络操作指导 在做PCB设计的时候需要更改铜皮的网络,Allegro上可以快速的更改铜皮的网络。如下图,需要给铜皮赋上网络 具体操作如下 选择selcet shape命令选中铜皮

会计毕业生的转行之路:坚持无畏,我是我自己的英雄

有时候&#xff0c;我们面对困境&#xff0c;总会犹豫&#xff0c;不敢迈出一步。 但当我们真的鼓起勇气打破困局时&#xff0c;才会发现出路就在眼前&#xff0c;原来只要不放弃&#xff0c;一切皆有可能。 初遇&#xff1a;会计生大四想转行 我是一名来自内蒙古的少数民族女生…