一、项目工程背景
串口之间的数据交互,是一个在实际工作当中应用非常广泛的场景,所以笔者在这篇博客里为大家详细介绍“串口八字节报文modbus crc收发校验”的工程案例,相信耐心地去系统实践完整个工程,一定会有比较大的收获。
首先向大家简单介绍一下串口通信八字节报文的由来,串口作为不同电路板之间SOC芯片的主要通信方式,本身也具有很大的普遍性,那么如何进行有效地数据交互呢,八字节报文的出现就很好地满足了数据交互量不大的实际应用的需求,虽然说芯片之间内部报文的组包和解包方式是可以自定义的,也就是说大家可以*发挥的,但是报文的定义也关系到整个嵌入式软件设计的复杂性甚至可靠性。
简单介绍八字节报文的具体定义方式,第一个字节代表通信地址,它的最高位永远是1'b1,换句话说地址0的地址位字节是8'h80,地址1的地址位字节是8'h81,地址2的地址位字节是8'h82,通常把广播地址位设置成8'hff,这样也就非常方便进行多板之间的485串口数据通信,同时把该字节的最高位置为1,程序设计上也很容易判断出报文的起始字节;第二个字节代表命令,因为本身一个字节含有8位,所以这里就可以代表255种命令,具体每一条命令可以由用户自己根据需求去定义;第三到第六字节,一共四个字节表示数据,即使是arm和fpga之间进行串口数据通信,而arm的float型等数据类型最多占四字节,所以这里用四字节报文则可以满足大部分数据交互的需求;最后两个字节为CRC校验,因为串口本身传输过程中也可能出现干扰等情况,加入modbus crc校验可以最大程度上提高报文传输的可靠性。
然后在“串口八字节报文modbus crc收发校验”项目工程中,动手实现一个基本的应用场景:用Artix7开发板的RS232串口先接收到上位机发送的8字节报文,如果确定该报文格式正确,即报文的第一字节的最高位为1,并且modbus crc校验正确,那么通过RS232串口把该报文重新发送给上位机反之就直接忽略该报文。通过这个项目工程,和大家系统性走过一遍FPGA开发流程,其中涵盖了功能代码的编写、系统仿真的测试、上板在线的观察等,下面我们也逐一去进行更加详细的说明。
二、功能代码的编写
1. crc16_modbus模块
整个工程按照模块细分为:uart_transfer、uart_receive、crc16_modbus、uartcrc16_loop四个模块,通过字面意思,大家也非常容易理解,即串口发送模块、串口接收模块、modbus crc校验模块、串口crc校验顶层模块,我们对每个模块展开说明,首先说一说crc16_modbus模块吧,因为该模块是整个项目工程中的核心模块。
谈到串口modbus的crc校验,最经典的应该就是两种实现方式,即通常所说的查表法和移位法。两种方法也各有各的优缺点,一方面,查表法的应用可以使得整体上crc计算代码更加简洁、也更加精炼,非常适合在MCU或者ARM端用C语言实现;但另一方面,对于FPGA端,如果再去使用查表法进行crc计算,那么一定会需要一个比较大的ROM表来存储数据,同时程序设计上也会因此带来非常大的不便,所以使用移位法更加适合Verilog代码的实现。
我们再来看一下移位法计算modbus crc的具体过程,这也是笔者从一本工具书上面摘录到的,大家可以看到计算过程中从第一步到第八步显得很公式化,同时也写得非常规范没有太多的二义性,所以紧接着我们会和大家一起去系统性地分析下整个过程,并且尝试着去用TimeGen画出其中的时序图,最后再根据TimeGen绘制的时序图,把下面文字性的流程用Verilog时序逻辑进行实现。
Modbus RTU CRC校验码计算方法:
1、 加载一值为0XFFFF的16位寄存器,此寄存器为CRC寄存器。
2、 把第一个8位二进制数据(即通讯信息帧的第一个字节)与16位的CRC寄存器低8位相异或,异或的结果仍存放于该CRC寄存器中。
3、 把CRC寄存器的内容右移一位,用0填补最高位,并检测移出位是0还是1。
4、 如果移出位为0,则重复第三步(再次右移一位);如果移出位为1,CRC寄存器与0XA001进行异或。
5、 重复步骤3和4,直到右移8次,这样整个8位数据全部进行了处理。
6、 重复步骤2和5,进行通讯信息帧下一个字节的处理。
7、 将该通讯信息帧所有字节按上述步骤计算完成后,得到的16位CRC寄存器的高、低字节进行交换。
8、 最后得到的CRC寄存器内容即为:CRC校验码。
笔者以前项目当中需要使用到FPGA和STM32串口通信,STM32端的modbus crc校验已经用查表法验证完成了,显然为了保证串口通信的可靠性,FPGA端也需要加入modbus crc校验,那时候也来来回回折腾了好几天,一方面网络上针对FPGA实现modbus crc校验基本就没有这方面的资源,看来看去不是说http://www.easics.com/webtools/crctool 可以直接生成校验码的,就是在讲查表法的具体流程等等,也积分下载了一些声称做出来的代码,但是基本都是欺骗积分的完全不是想要的,总的来说没有太多有价值的信息,当然上面的网站也看过了,很可惜人家就压根就没有生成modbus crc校验选项。当时真的很是崩溃,但因为是现实项目要求硬着头皮也需要搞出来,所以笔者想把这份代码贡献出来,相信它也一定会帮助到很多有这方面需求的朋友们。
然后我们再来仔细读一读上面Modbus Crc校验码计算方法,笔者真诚地建议大家最好代入一字节数据去实际推导下整个流程,搞清楚每个步骤是在做什么会得到什么,当时笔者也被上面的计算方法整得很懵很晕,但是因为懒,没错就是因为懒,想去跳过TimeGen画波形图的流程,直接通过不断调试硬写出来,但很可惜折腾了两天都失败了。
我们带入一个字节8'h80,去进行modbus crc校验,也请大家一起拿着纸和笔去实际动手推导一下,整个流程即使都用笔算十分钟都够用了。这里也补充一下对于a、b的异或运算:如果a、b两个值不相同,则异或结果为1;如果a、b两个值相同,异或结果为0。
我们定义一个16位的crc_reg寄存器初始值为16'hffff,即按照上述步骤1所述,接着来到了步骤2,对8'h80和crc_reg的低8位相异或,再把结果存到crc_reg,即可以得到crc_reg值为16'hff7f,然后我们去重复8次的步骤3和步骤4这样就把整个8位数据都进行了移位处理。
笔者在这里和大家一起去笔算整个流程,其中经过步骤3后,如果移出位是0的,都用蓝色进行标出如图10-13所示,如16'h6fdf、16'h4bf7、16'h42fd,方便大家观察波形。
1.crc_reg值为16'hff7f,右移一位即为16'h7fbf,移出位是1,跳转到步骤4,与16'ha001进行异或,得到16'hdfbe;
2. crc_reg值为16' hdfbe,右移一位即为16'h6fdf,移出位是0,跳转到步骤4,得到16'h6fdf;
3. crc_reg值为16' h6fdf,右移一位即为16'h37ef, 移出位是1,跳转到步骤4,与16'ha001进行异或,得到16'h97ee;
4. crc_reg值为16'h97ee,右移一位即为16'h4bf7,移出位是0,跳转到步骤4,得到16' h4bf7;
5. crc_reg值为16' h4bf7,右移一位即为16'h25fb,移出位是1,跳转到步骤4,与16'ha001进行异或,得到16'h85fa;
6. crc_reg值为16'h85fa,右移一位即为16'h42fd,移出位是0,跳转到步骤4,得到16' h42fd;
7. crc_reg值为16' h42fd,右移一位即为16'h217e,移出位是1,跳转到步骤4,与16'ha001进行异或,得到16'h817f;
8. crc_reg值为16'h817f,右移一位即为16'h40bf,移出位是1,跳转到步骤4,与16'ha001进行异或,得到16'he0be。
经过8次步骤3和步骤4的计算,crc_reg就已经得到了字节8'h80的modbus crc校验结果,后面的字节也按照上述流程进行,重复步骤2到步骤5,当crc校验到最后一个字节时候,我们需要将得到的16位crc_reg寄存器的高、低字节进行交换即可,如图1所示,我们使用了CRC Calculator小插件进行了8'h80的modbus crc校验,大家可以看到结果恰好为16'he0be,而CRC Calculator只是没有进行步骤7,对CRC寄存器的高、低字节数据交换而已。
图1 用CRC Calculator插件进行字节8'h80进行modbus crc校验的结果
如下图2所示,笔者已经用TimeGen为大家绘制好了字节8'h80进行modbus crc校验中各个信号的波形图,接下来我们会根据整个波形图进行crc16_modbus模块的Verilog代码编写,大家也可以回顾一下上面的步骤,然后对照图3,再去简单思考总结下整个流程,有了上面modbus crc详细的计算流程以及以上的笔算推导,相信大家也都完全可以参考图2去设计好这个模块的状态机和计数器。
我们根据上面modbus crc的计算流程,把整个模块分成6个状态, IDLE即空闲状态,CRC_XOR即上面的步骤2,CRC_CIRCLE即上面的步骤3,CRG_0REG代表步骤3中移出的位是0,而CRG_1REG代表步骤3中移出的位是1,CRC_DONE即该字节已经完成了modbus crc校验。因为每个字节都需要进行8次移位才能最后得到modbus crc校验结果,所以在这里,我们定义了一个bit_cnt计数器,那么它的加一条件和结束条件也都非常清晰了,即add_bit_cnt = (state_c==CRG_0REG || state_c==CRG_1REG); end_bit_cnt = add_bit_cnt && bit_cnt== 8-1,再分别去设计好本模块的关键信号crc_reg、crc_dout、crc_dout_vld即可,根据上面的逻辑按照参考图2即可设计出来,表1即为本模块的信号列表,图3所示即为整个模块的详细代码设计供大家参考。
信号列表 | ||
信号名 |
I/O |
位宽 |
clk |
I |
1 |
rst_n |
I |
1 |
sclr |
I |
1 |
din |
I |
8 |
din_vld |
I |
1 |
crc_dout |
O |
16 |
crc_dout_vld |
O |
1 |
表1 串口收发的八字节数据报文CRC校验设计中的信号列表
图2 字节8'h80进行modbus crc校验中各个信号的波形图
图3 crc16_modbus模块的代码设计
2. uart_receive和uart_transfer模块
如图4所示是串口的底层实现:在空闲状态时,串口的数据线会一直保持在高电平状态;当主机要发送数据时,会将数据线拉低一个波特率的时间,从而告诉从机有数据要传输了,要做好准备;起始位之后是数据位,数据位的位数由双方之前约定好即可,双方约定后才能正确地传输和解析报文。每个数据位传输时都会占用一个波特率的时间,请注意在这里是从低位到高位进行传输,比如要传输数据4'b0101,在串口传输时是先传输最低位的“1”,数据传输完成后,发送检验位:奇偶校验是一种非常简单常用的数据校验方式,又细分为奇校验和偶校验,但是一般在实际项目工程中使用的不多,所以不做详细介绍,在校验位后即为最后一位停止位,主机必须保证有停止位,即把数据线拉高一个波特率的时间。因为数据在传输线上传输,硬件上可能会有一定的干扰,每一个设备内部又有自己的时钟,很可能在通信中两台设备间出现了一些细微的不同步,停止位的到来表示整个报文传输的结束,也使得从机可以正确地识别下一轮报文数据的起始位。
在串口传输当中,有一个非常重要的概率即波特率,串口中常用到的波特率有9600、19200、57600、115200,波特率表示每个数据在传输线上的传输速率,比如在9600的波特率下,每位数据需要传输1/9600s=104166ns的时间,所以在两个设备之间串口通信之前,都需要事先约定相同的波特率、是否有校验位、包文的长度或者包文的结束字节等等。
图4 串口通信底层实现方式
我们首先来看串口接收模块uart_receive,这里我们提前约定每包报文是1字节即8bit,只需要把图4所示的串口通信底层实现方式还原成verilog代码即可,我们用两个计数器cnt0和cnt1去分别计数一个波特率的时间即接收1bit时间和计数9bit时间,因为这里我们把起始位1bit和停止位1bit也算在了接收数据当中,所以cnt1需要计数10次,rxd_data在cnt1计数在1-9的时刻,使用数据拼接的老办法即可得到,但是需要大家注意一下,为了防止偏差一般串口都采用计数到一半波特率的时间去做赋值操作,在数完end_cnt1即接收完8bit数据和1bit停止位后,拉高rxd_data_vld一个时钟周期即可,如表2和表3分别为uart_receive模块和uart_transfer模块的信号列表,串口接收模块uart_receive的代码设计如图5所示。
信号列表 | ||
信号名 |
I/O |
位宽 |
clk |
I |
1 |
rst_n |
I |
1 |
rxd_uart |
I |
1 |
rxd_data |
O |
8 |
rxd_data_vld |
O |
1 |
表2 uart_receive模块信号列表
信号列表 | ||
信号名 |
I/O |
位宽 |
clk |
I |
1 |
rst_n |
I |
1 |
txd_data |
I |
64 |
txd_data_vld |
I |
1 |
txd_uart |
O |
1 |
表3 uart_transfer模块信号列表
图5 串口接收模块的代码设计
在uart_transfer模块,如果顶层模块对八字节报文的modbus crc校验正确,我们则需要依次把这八字节逐一字节地送到上位机上,所以需要增加一个计数器cnt2,去计数向上位机发送到了第几个字节。在这里也用一个常用的老方法,去定义一个tx_data_shift,每次add_cnt2的时候去进行移位操作,最后把要发送的10位数据打包发送即可,其中包含了每次的一字节数和前后起始位和结束位,即uart_tx_data = {1'b1,tx_data_shift[63:56],1'b0}。
图6 uart_transfer模块的代码设计
3. uartcrc16_loop模块
uartcrc16_loop模块即顶层的例化模块,在这里我们需要把uart_transfer、uart_receive、crc16_modbus三个模块的相关信号都例化起来,同时本模块也应该对接收到的报文进行判断,是否报文首字节的最高位为1,是否整个八字节报文modbus crc校验正确,如果正确需要把该八字节报文重新发送给上位机端,如表4所示,是本模块的信号列表,所以在本模块我们也设计了一个状态机去对接收报文进行判断,设计状态机的状态为IDLE即空闲状态,RECEIVE_DATA即接收八字节报文的前六字节数据状态;RECEIVE_CRC即接收八字节报文的后两字节CRC状态;CRC_RIGHT即接收到的八字节报文通过modbus crc检验;而对应的CRC_ERROR即接收到的八字节报文没通过modbus crc检验。整个模块设计中我们把每个报文的前六字节数据代入crc16_modbus模块进行计算,再接收完该报文后面两字节的crc数据后,同时与计算结果进行对比,如果计算结果和接收结果是一致的那么就跳转到CRC_RIGHT状态,不一致就跳转到CRC_ERROR状态,同时在判断过该报文正确后,置位信号txd_data_vld一个周期的高电平,并把八字节的报文数据例化到uart_transfer模块中,最后由uart_transfer模块向上位机逐一发送八个字节的报文。
信号列表 | ||
信号名 |
I/O |
位宽 |
clk |
I |
1 |
rst_n |
I |
1 |
rxd_uart |
I |
1 |
txd_uart |
O |
1 |
表4 uart_transfer模块信号列表
如图7是 uartcrc16_loop模块的详细代码设计供参考,只需要注意好各个模块的信号名不要例化错了,再把本模块对八字节报文的modbus crc判断状态机设计好即可,这里也没有太多绕脑的地方。
图7 uartcrc16_loop模块的代码设计
二、系统仿真的测试
对于串口收发的八字节数据报文CRC校验的Testbench设计,首先我们肯定是需要数据源就是很多组八字节的报文,作为激励去验证,那么如何去大量产生这些数据呢,笔者直接把STM32计算modbus crc的代码搬运到VC++6.0当中,去生成一个crc16.txt文本文件,在STM32端,我们使用了查表法进行modbus crc校验码的计算,当然这里面的C代码设计笔者不想在此展开过度叙述了,大家多多少少都有一些C语言基础,整个代码不是非常难懂,而且笔者也尽量写得简单明白,直接运行整个工程,即可以生成一个crc16.txt文本文件,里面保存了1020条随机命令,随机数据的八字节报文,其中是地址0、1、2和广播地址各255条,作为我们Testbench的激励,如下图8所示,是整个VC++6.0工程的界面框图。
图8 VC++6.0下产生Testbench的激励
然后打开VC++6.0工程目录下的crc16.txt文本文件,如图9所示,即可以看到1020条生成的随机命令,随机数据的八字节报文,这里我们就遇到了一个问题,那就是如何有效地编写Testbench测试文件,把txt文件中的报文代入功能模块uartcrc16_loop中,并判断校验结果是否正确,根据结果在Modelsim的功能框下打印出结果,大家还记得前面所说过的$fopen、$fscanf、$fclose吗,我们可以在这里用来打开crc16.txt并读取里面的数据。
如下图10所示,在Testbench测试文件中,我们去读取crc16.txt文件中的1020个数据,并把它们保持到位宽为64的testdata[0:1019]中,类似串口自收自发的Testbench,这里我们需要把八个字节数据逐一发送出去进行拆包,然后再逐一接收回来进行组包,最后当rs232_flag处于下降沿的时刻,即串口已经接收完8字节数据,再对接收数据和发送数据进行比较,如果一致则认为功能模块modbus crc校验无误,并打印“ture”信息,如果不一致则认为功能模块modbus crc校验有误,并打印“error”信息,大家可以参考一下如图10所示的Testbench设计,代入Modelsim后,弹出如图11的仿真结果,可以看到在Modelsim的窗口界面下一直在打印“ture”即modbus crc校验都正确。
图9 crc16.txt文本文件下的八字节报文
图10 串口收发的八字节数据报文CRC校验的输入信号激励设计
图11 串口收发的八字节数据报文CRC校验的仿真结果
三、上板在线的观察
上板在线观察也是FPGA开发当中非常重要的步骤,Vivado下支持添加ILA IP核,通过把想要观察的信号例化到ILA IP中,即可以在线观察实时观测到波形,其原理也已经在前面的章节中叙述过了,大家可以把ILA看成在线逻辑分析仪,如图12所示,我们在Vivado环境下配置ILA IP核,然后再在uartcrc16_loop、crc16_modbus模块下把想要观察的信号例化到ILA IP核里,如下图13所示,然后我们生成.bit文件下载到Artix7开发板中,具体步骤前面的章节有详细叙述,大家可以简单回顾下,当然也可以直接单击PROJECT MANAGER菜单栏下的Generate Bitstream即可生成.bit文件。
把.bit文件下载烧录到Artix7开发板中,如图14所示,我们可以在Vivado下实时观察ILA IP核例化来的信号,这里举个例子,我们通过设置去抓取uartcrc16_loop模块下的state_c信号为3的时刻,即CHECK_CRC检查报文modbus crc是否正确,如图15所示,直接把一个正确的八字节报文通过上位机的串口调试助手RS232发送到Artix7开发板,然后大家可以观察到状态机直接由CHECK_CRC跳转到CRC_RIGHT状态,再由CRC_RIGHT状态跳转到IDLE状态,同时Artix7开发板也通过RS232向上位机发送了相同的八字节报文,从而也验证了我们整个功能模块设计上的正确。
图12 Vivado下添加ILA IP核
图13 在uartcrc16_loop、crc16_modbus模块下例化ILA IP核
图14 Vivado下实时观察ILA IP核例化来的信号
图15 串口助手发送八字节报文