OpenEdv-开源电子网

 找回密码
 立即注册

扫一扫,访问微社区

正点原子全套STM32开发资料,上千讲STM32视频教程,RT1052教程免费下载啦...
楼主: xiatianyun

学习精英版的学习笔记

[复制链接]

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-12 10:58:23 | 显示全部楼层
目前我努力的方向是实现RS485的Modbus-RTU通讯,和我的PLC进行数据传输。
我现在可以想象的问题是:
通讯的地址是如何处理?
主站和从站在设置上有什么特点?
发送和接收如何才能在一帧传输后才来中断处理而不是一个字节传输就中断处理?
通讯协议是如何解析的?
协议是如何封装成数据帧的?
怎样更高效优雅地解析协议?
回复 支持 反对

使用道具 举报

  离线 

24

主题

1180

帖子

2

精华

金牌会员

Rank: 6Rank: 6

积分
1962
金钱
1962
注册时间
2018-5-11
在线时间
369 小时
发表于 2018-7-12 20:30:58 | 显示全部楼层
xiatianyun 发表于 2018-7-11 17:27
精英版没有直接的RS232接口,是把芯片的串口经过转换成为USB接口,使用USB来模拟串口的,需要在电脑端安装R ...
学习了。
个人理解,是不是全双工主要指的是信道,
如果收和发是双向互不影响,同时可进行的,就是全双工
至于电脑的处理是另外的事情,处理不过来或方法不当是终端资源和人为失误
https://github.com/ShuifaHe/STM32.git
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-13 10:57:24 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-7-13 16:30 编辑

以串口1为例来学习。
发送数据口TX是PA9,该端口配置成复用推挽输出模式。数据首先被送到发送数据寄存器TDR,然后被自动送入发送移位寄存器,每个bit在时钟驱动下由发送控制器顺序发到TX口发送出去。
接收数据口RX是PA10,该端口配置成浮空输入模式。数据的每个bit从RX口由时钟驱动下被接收控制器送到接收移位寄存器合成数据,然后送到接收数据寄存器RDR中,然后被程序接收。
TDR后RDR其实在系统中是同一个寄存器寻址DR,如何判断是哪个呢?
如果是接收,也就是读DR,则读取的是RDR。如果是写DR,则写的是TDR。
系统就是根据读写来分别对RDR和TDR进行操作的。
DR是个32位寄存器,但只有低9位有效[9~0]。之所以不是低8位,就是因为有个奇偶校验位存在,如果没有校验,则是低8位。
奇偶校验是由硬件自动完成的,不用设计程序。如果校验失败,则可能会产生中断以便程序处理。

从框图可知,接收和发送没有多余的缓冲区。只有一个缓冲,也就是DR。需要程序来提供适当的数据缓冲。
------------------------------
时钟如何确定?
串口1的时钟来源与PCLK2,也就是APB2,开发板是72MHz。
其他串口2~5时钟来源与PCLK1,也就是APB1,是36MHz。
串口的波特率是在时钟驱动下完成的单位时间1s内的bit传输率,常用的有9600、19200、115200等等。
知道了所需的波特率如何设置呢?
使用的是BRR寄存器设置,BRR的值被看成定点数。定点数比浮点数简单,用于寄存器设置完全够用了。
为什么还要有定点数设置波特率呢?因为PCLKx不直接用来驱动数据传输,而是PCLKx的时钟频率先经过BRR分频再经过16分频得到的时钟信号再驱动数据传输的。
也就是BRR只是波特率构成的一个变因,具体公式是:BoundRate=PCLKx/(16*BRR)。
则BRR=PCLKx/(16*BoundRate)
说过BRR是定点数,0~3位是小数部分,4~15位是整数部分。
计算就不用了,使用库函数设置串口参数后进行串口初始化会自动计算的。具体是USART_Init().
---------------------------------
使用串口通讯可以不断地查询一次通讯是否完成来处理,也可以用中断来处理。
做PLC程序时用串口通讯我一般采用两种方式,一种是定时中断,定时到查询状态,一种是不断查询状态。
定时中断来进行通讯大多用于主站端,定时发送数据到总线上。而从站如果采用定时中断接收数据存在问题,会有错过数据的情况,所以从站一般是不断查询。
而STM32有提供接收和发送中断,可以在中断后处理。
串口中断其实每个串口只有一个中断号,即USARTx_IRQn,共5个。
但每个串口中断可以可以由很多个标识位来触发,需要配置由哪个标识位来触发中断。
常用的触发事件是:
TXE:发送数据为空,表示发送数据寄存器空,可以接受新数据发送。通过读手册发现是TDR数据转移到发送移位寄存器后产生该标识。产生该标识时可以接受新的发送数据(1byte)。
TC: 发送完成。可能是指发送移位寄存器里面的数据发送完毕,其后新的发送数据来自发送数据寄存器TDR。通过读手册发现是用于多数据通讯中,此时一帧发送完毕且TXE被置位。系统是怎么判断我有一帧数据发送的呢?
RXNE:准备好读取接收到的数据,可能是指接收移位寄存器数据合成完毕被送往RDR,可以读取RDR了。如果不读则该数据会被后来的数据覆盖。
PE:奇偶校验失败。失败后可能需要重新发送相同的指令重新读取数据。具体如何重新读取,这个和协议有关。
-----------------------------
在学习外部IO口中断时在中断服务函数结束前需要复位中断标识位以便中断继续,但串口通讯可以由读取或写入DR操作来自动清楚标识位,不用手动清除。当然,也可以手动清除。




回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-13 13:13:12 | 显示全部楼层
写一个简单的串口通讯试验的流程是:
确定用哪个串口,串口1的话就是用PA9/PA10,当然复用重映射也是可以的。
查手册看通讯用的外设GPIO口需要设置成什么IO模式。
首先是端口初始化:
GPIO口时钟使能、串口时钟时钟。
GPIO端口工作模式设置初始化。
串口初始化。
串口使能。
如果需要使用串口中断的话还需要串口中断初始化,使串口中断和中断事件绑定。
中断向量初始化,确定串口中断号和优先级。之前必须先设置好优先级分组采用哪个分组模式。
编写串口中断服务函数,中断函数名应当和系统初始化的向量名称一致。
最后就是编写数据解析和合成。
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-15 19:51:47 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-7-15 20:57 编辑

找了两个Modbus通讯程序来看,看得一头雾水。
可能是Modbus本身就不是一个统一的通讯协议吧,各家根据自己的需要可以稍作修改以适应需求,所以和我以前认识的有所偏差。
还是老实地一步一步来吧。
先来看后面C盘的485教程,发现原来485只是串口外接的一个电平转换电路,和232道理一样。
不过还是有不一样的地方:232不用控制发送和接收,因为232是全双工的,而485则需要人工控制收发,需要在程序里面来协调收发步骤。具体就是通过PD7口来控制的。
所以,需要接收时除了程序接收外还需要控制PD7口为0,需要发送时除了程序发送外还需要控制PD7口为1.
其他的和普通串口通讯一样的配置。
精英版的485口从USART2引出,使用的是复用功能的PA2/PA3口。
RS485的引入其实是为了解决RS232的一些问题,首先是通讯距离短,最大也就50来米,短距离通讯没问题,但使用在工业上就难以应付了。其次是RS232使用的通讯电压比较高,正负12伏,这个很可能对通讯接口造成损伤,导致设备损坏。
RS485通讯距离比较远,1km也是可以的,当然距离长波特率就低。如果又需要距离远有需要一定的波特率就每隔500m加通讯中继器。当然,这也不是任意的。
RS585还有个特点,在链路上的两端需要加终端匹配电阻,并且串口通讯是不能做成环网的也不可以是星型。
-----------------------------------------
还是来认识一下Modbus-RTU通讯协议吧。

回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-16 21:36:54 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-7-16 22:52 编辑

今天看了下Modbus通讯相关手册,重点是数据帧部分。Modbus的核心数据帧为:功能码+数据
根据不同的实现添加了一些其他的域,在串行通讯上的Modbus帧为:地址域+功能码+数据+CRC校验。也即添加了地址域和校验域。
一帧数据通常还包含帧起始和帧结束部分,以便对方知道是否开始接收。
Modbus是主从通讯协议,链路上只能有一个主站,可以有多个从站。
只能由主站发起通讯,从站被动应答。可以类比从站为服务器而主站是客户机,只是服务器有多个而客户机只有一个。
同一时刻主站只会发起一个通讯事务,如果不是广播则只会是向一个从站通讯,而从站间是不会互相通讯的。
从站间通讯必须经过主站中转。这种中转不是协议的一部分,而是程序通过两次通信来处理。
--------------------------------
Modbus是应用型协议,可以搭载在串行也可以是TCP。
Modbus的串行链路分两种细分模式:RTU和ASCII模式。
我觉得RTU和ASCII除了数据的表现方式不同外,还有帧起始和结束很不同。
ASCII的帧启停很好理解,就像规定一样,遇到冒号就是帧起始,需要接收接下来的数据,遇到回车换行接意味着帧结束。这个结束和教程串口通信试验一样。
Modbus-RTU的比较特别,它并没有规定用什么来做帧启停标识。当接收到第一个字节时就是帧起始,而帧结束是用时间来检测的。
RTU规定,帧间必须要有不小于3.5个字符传输的时间做间隔。又规定,帧数据必须是连续字节流,什么是连续呢?
字节和字节间的时间间隔必须小于1.5个字节传输时间。
这样可以理解为:链路上的各节点只要统一了间隔时间就能很好地识别帧起始和帧结束。
这不统一呀。同一个链路如果不是统一开发的呢?比如有第三方设备呢。而这是RTU的常态。
手册上建议,如果比特率超过19200bps,那么最后固定时间间隔。帧间隔t3.5=1.75ms,字节间隔t1.5=750us。
这样Modbus-RTU就和定时器搭上了关系。
接收一个字节完毕就复位t1.5定时器开始定时,如果在定时器还没有ON时就接收到了下个字节则字节有效,再次复位t1.5定时器开始定时。如果t1.5ON则认为其后接收的字节数据无效或帧数据终止可以做其他处理了。比如开始判断是不是发到本站的、CRC校验等等。
收到t1.5ON时紧接着开始t3.5定时,t3.5ON时认为帧结束。
发送则只用到t3.5定时器,因为字节发送在本端检测字节是否连续没有多少作用,通常会极快结束。t1.5字符连续主要用于接收检测。
发送的结束是主动的,一开始就知道哪里结束,所以结束时开始t3.5定时,定时ON进入空闲等待下个处理事务。
所以,接收比较起发送来就复杂得多。
如果接收字节时出现t1.5ON,进行判断是否发到本站的。如果不是,那么前面接收的数据就多余了。所以,最好在帧起始接收到帧地址数据时就进行判断,如果不是就回到空闲,也不用继续接收了。
那么说来,其实是靠程序来判断地址的,即使不是发到本站的也可以接收数据,并没有强制机制。全靠自觉。
对了,Modbus地址域的地址是从站地址,主站没有地址。地址为1个字节,范围就是0~255。但是,并不是说所有地址都可用,有些是保留地址,至于做什么,不知道。
链路在没有中继情况下最大可以挂接32台设备,包括主站。这主要是从可靠性出发来约束的,如果有中继则可以挂更多设备。


回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-16 23:36:26 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-7-17 14:25 编辑

串口通信配置的校验位又是怎么回事呢?
其实串口配置的波特率、校验、停止位都是配置硬件。
这些由串口硬件自动完成,不必编程。
当配置好后,一个字节其实发送接收的不是一个8位字节,而是11bit:1bit起始位、8bit数据位、1bit奇偶校验位、1bit停止位。如果没有校验则校验位填充停止位。
至于启停位是什么值由硬件来处理,不必纠结。
程序只需处理发送的8bit数据和结束8bit数据,想接收数据位外的位也是不能做到的。
如果出现校验失败,会有中断产生(如果开启的话),有标识位置位,可以在收到时处理。(丢弃或发起重发指令)
同样,帧校验失败也是丢弃或发起重发指令。不过帧校验需要编程。
----------------------
Modbus-RTU数据帧:
地址1byte + 功能码1byte + 数据0~252byte + CRC校验2byte.
CRC由2字节组成,LSB在前HSB在后,这和大多数多字节数据一致。
这样,一个RTU帧最大256字节。数据帧是可变长度的。
这些是Modbus-RTU通信的基本知识,有了这些东西就可以完成基本Modbus-RTU通信的编程了。
接下来就是功能码和数据域打包和解包了。
这也是RTU的关键。
---------------------------------------
CRC校验算法有两种做法:通用算法和查表算法。我觉得通用好理解些。
不过CRC校验的原理是什么呢?
-------------

看了半天,也没有看出CRC校验的原理,网上搜的大多是实现方法。
还是检现成的吧,有时间再看看能不能看懂CRC校验原理。
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-17 22:23:00 | 显示全部楼层
要用到定时器做通信,以前学的是SysTick,现在学习定时器。
STM32F10x一共有8个定时器TIMx,分三种,最简单的是基本定时器TIM6和TIM7,通用型是TIM2~TIM5,高级定时器是TIM1和TIM8.
其实定时器的基本定时原理和以前我用SysTick实现的通用延时定时器一样的道理,就是对定时计数器的时钟脉冲进行计数,计数值可以从0开始到预置值,也可以从预置值倒计数至0,还可以0到预置值再从预置值到0反复来回计数。
这样就有了计数模式:向上、向下、中间(向上和向下)三种。
不过基本定时器TIM6/TIM7只有向上一种计数模式。
计数到达预置值产生中断,是TIM_IT_Update,叫中断更新。可以看成是中断事件,教程里叫做中断源。
其实,除了高级定时器有多个中断号外其他6个都只有一个中断号:TIMx_IRQn.
最常用的就是TIM_IT_Update了,当计数至模式设定的方向值时产生中断,但是并不需要手动更新装填预置值,会自动更新的。
------------------------
来看看,计数时钟。
计数时钟是CK_CNT,其时钟频率叫CK_CLK。其实我也搞不清有什么区别,为什么叫不同的名字,一会CK_CNT,一会CK_CLK。
计数时钟来源于TIMxCLK 的PSC分频器,PSC就是分频值。
TIMxCLK时钟就是定时器时钟,其实只是定时器间接时钟,不能把它看作定时器时钟,定时器时钟应该就是CK_CLK,这个才是定时计数器的直接计数时钟,时间就由CK_CLK决定。
TIMxCLK和系统初始化后的APB1或APB2有关。
如果是基本定时器则和低速总线APB1有关,如果APB1的预分频值不是1则TIMxCLK是APB1的2倍频,精英版就是这种情况,APB1是36MHz,那么TIMxCLK就是72MHz.
这样,CK_CLK=TIMxCLK/(PSC-1) ,如果需要一个10kHz的计数时钟,则10k=72M/(PSC-1),PSC=7199.
10kHz时钟的周期是1/10k s,即0.1ms。
如果要定时中断在500ms时产生,则预置值RCC*0.1ms=500ms,预置值为5000。实际应该少1,RCC=4999.
基本型定时器除了定时没有太多功能,简单实用。我打算用TIM6做Modbus-RTU通信的t3.5用TIM7做t1.5。
也可以用SysTick来实现,不过SysTick脉冲计数型有误差,可能是1个脉冲周期。
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-21 12:03:17 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-7-29 22:10 编辑

陆陆续续学习了RS485通讯的基本知识。现在来实现,记录下过程中的一些关键点。
首先实现Modbus-RTU的通讯收发,至于协议如果解码编码,下一步了。
1、RS485端口初始化。
这一步使精英板所带RS485端口初始化,有几个点需要注意。除了像RS232普通串口一样初始化USART2外,RS485是半双工通讯,电平转换芯片需要一个信号来切换收发,这个是PD.7口,0信号是接收数据1信号是发送数据。PD.7口使用位带操作来读写比较方便。
主站和从站都初始化为接收数据模式。本来主站应该初始化为发送数据才适合,不过收发还是需要初始化外的程序来控制的,所以干脆一律初始化为接收。
初始化除了需要初始化PD.7外还需要初始化两个定时器TIM6和TIM7,这两个都是基本定时器,TIM7用来作为通讯中检测字节连续性的t1.5定时器使用,而TIM6作为帧间间隔t3.5使用。t1.5的作用像看门狗,如果定时时间内检测到字节数据就复位定时器重新开始定时,如果t1.5动作则认为帧接收结束。实际上t1.5动作可能是帧结束也可能是通讯中出现故障不能及时收到数据,无论如何都判定帧结束可以做帧解析了。TIM6和TIM7初始化为100kHz,这样PSC为719。重装载寄存器为1750和750,也即1.75ms和750us。可以更大。定时器初始化后不使能,由外部程序在适当时候启动定时。
2、接收逻辑。
使用接收中断,中断服务函数中开始判定系统故障标识位是否置位,置位则置位出错标识位不作进一步处理,也可以直接结束数据接收。暂时接续接收数据,因为出错,接下来的帧有效性判断会失败。如果无错就转储数据到缓冲区。转储后复位t1.5并启动t1.5定时看门狗。
t1.5定时到达后复位t1.5并失能停止定时。接着复位t3.5并启动t3.5定时器。然后进行帧有效性判断(CRC16校验),校验正确进行解码。解码是否放在t1.5中断内还没有明确。
如果t3.5定时达到,则帧间隔被添加,复位t3.5并停止t3.5工作。接收终止。
解码可以不放在t1.5内,外部程序不断读取帧结束标识和可以读取数据标识再进行解码。
t1.5中断服务函数:
[C] 纯文本查看 复制代码
void TIM7_IRQHandler(void)
{
    if(TIM_GetITStatus(T1_5, TIM_IT_Update) == SET)
    {
        //复位t1.5并停止工作。
        TIM_ClearITPendingBit(T1_5, TIM_IT_Update);        
        TIM_SetCounter(T1_5, 0); //复位向上计数器当前值为0.
        TIM_Cmd(T1_5, DISABLE); //使能定时器开始定时.
        
        //t1.5中断时启动t3.5定时器,监测帧是否结束.
        TIM_ClearITPendingBit(T3_5, TIM_IT_Update); //清除定时器中断更新标识.
        TIM_SetCounter(T3_5, 0); //复位向上计数器当前值为0.
        TIM_Cmd(T3_5, ENABLE); //使能定时器开始定时.
        
        //判断数据帧的有效性.
        //Modbus_Control_Struct.bFrameOk = Pkg_Validity();
        //如果帧有效则进行接收到的包解码.
        if(Pkg_Validity())
            Pkg_Uncode();
    }
}


t3.5中断服务函数:
[C] 纯文本查看 复制代码
void TIM6_IRQHandler(void)
{
    if(TIM_GetITStatus(T3_5, TIM_IT_Update) == SET)
    {
        //复位t1.5和t3.5定时器并失能,停止定时监测.
//        TIM_ClearITPendingBit(T1_5, TIM_IT_Update);
//        TIM_SetCounter(T1_5, 0); //复位向上计数器当前值为0.
//        TIM_Cmd(T1_5, DISABLE); //使能定时器开始定时.
        TIM_ClearITPendingBit(T3_5, TIM_IT_Update);
        TIM_SetCounter(T3_5, 0); //复位向上计数器当前值为0.
        TIM_Cmd(T3_5, DISABLE); //使能定时器开始定时.
        
        //如果是接收状态的帧结束,则置位接收结束标识。
        if(Modbus_Control_Struct.u8Status == 2)        
            Modbus_Control_Struct.bRxEnd = TRUE;
        //如果是发送状态的帧结束,则置位发送结束标识。
        if(Modbus_Control_Struct.u8Status == 1)
            Modbus_Control_Struct.bTxEnd = TRUE;
        
        //设置系统状态进入空闲.        
        Modbus_Control_Struct.u8Status = 0;    
        Modbus_Control_Struct.bBusy = FALSE; 
               
    }
}

3、发送逻辑。
发送是主动的,填充发送缓冲区后发送。先进行发送失能,然后发送。发送后启动t3.5添加帧间隔。
[C] 纯文本查看 复制代码
//发送数据帧
void SendFrame(ModRTU_TX_Struct TX_Struct)
{
    RS485_TXENB; //发送使能
    Modbus_Control_Struct.bTxEnd = FALSE;  //复位发送结束标识。
    Modbus_Control_Struct.u8Status = 1;
    
    for(u8 i = 0; i < TX_Struct.u16Index; i++)
    {
        USART_SendData(USART2,TX_Struct.Buffer[i]);
        while(USART_GetFlagStatus(USART2,USART_FLAG_TXE) != SET);
    }        
    //等待全部连续数据发送完毕。
    while(USART_GetFlagStatus(USART2, USART_FLAG_TC) != SET);
    
    //发送缓冲区清空。
    TX_Struct.u16Index = 0; 
    
    //帧间延时,开启t3.5。
    TIM_ClearITPendingBit(T3_5, TIM_IT_Update); //清除定时器中断更新标识.
    TIM_SetCounter(T3_5, 0); //复位向上计数器当前值为0.
    TIM_Cmd(T3_5, ENABLE); //使能定时器开始定时.
    //等待帧间隔结束。
    while(Modbus_Status_Struct.bBusy);    
}


----------
接收:
[C] 纯文本查看 复制代码
//接收数据帧
void ReceiveFrame(ModRTU_RX_Struct RX_Struct)
{
    //清空接收缓冲区。
    RX_Struct.u16Index = 0;
    
    RS485_RXENB; //接收使能
    Modbus_Control_Struct.bRxEnd = FALSE; //复位接收结束标识。
    //数据帧接收后会自动添加延时。
}

回复 支持 反对

使用道具 举报

  离线 

0

主题

3

帖子

0

精华

新手入门

积分
19
金钱
19
注册时间
2018-7-21
在线时间
1 小时
发表于 2018-7-21 18:16:33 | 显示全部楼层
HAOXIGUAN,坚持下去,就能成功啦
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-21 18:30:57 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-7-21 18:32 编辑

Modbus-RTU模式的非协议通讯终于调通了。
有几点体会:
1、CRC16校验并不能自动校正通讯数据错误,仅仅是判定发生了数据错误。
2、CRC16的多项式根据不同的标准有多个选择,CRC16_Modbus标准只是其中之一。其要求是:CRC16初始值为0xFFFF,多项式为0xA001,其实应该是0x8005,是该值逆序重排值。
多项式在主站和从站均必须固定不变,如果有节点不是这个值就不能通讯成功,因为不同的多项式其CRC校验结果值不同。
我为了调试CRC16把传入的数据(含CRC)又作了CRC16校验,然后把数据发到助手,发现后面添加的CRC值为0x00。这也是CRC校验的目的。把含CRC的数据再校验结果为0.
3、用助手定时发送数据到开发板会偶尔产生校验错误,我认为是定时的问题。因为不同步,助手自发自的数据,而开发板收到数据后回传给助手,开发板收发是步调协同的,而助手只按时发送,就导致了开发板发送时可能是助手发送时,而开发板接收时可能助手已经在发送中了,导致不能完整接收数据。调整定时时间也是如此,只是多少频率的问题。
所以,我想如果是半双工,从站和主站是需要协同才能正常工作的。以前遇到过从站一味发送数据,不管有没有站点提出请求的情况可能是RS232.也许我记不清了。

--------------------------
下面学习Modbus-RTU数据解析,功能码、数据地址、打包、解包。
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-22 09:33:14 | 显示全部楼层
先来试验功能码0x10.
0x10是写连续多个保持寄存器。
主站发送的请求数据帧格式:
MB_Addr 1B + 0x10 1B + 元件基址 2B + 元件数量 2B + 后续数据字节长度N 1B + 可变数据域  N*2B  + CRC 2B
一共最大通讯长度为256B,则可变的数据占用最大为256-9=247B ,如果HoldReg一个元件需要2B数据赋值的话,只能是247/2=123个元件。也即,一次通讯最大写123个连续元件。
即元件数量最大123=0x7B。后续数据长度最大N为246=0xF6。
元件数量为何要占2个字节?
从站响应帧结构:
如果正常响应:
MB_Addr 1B + 0x10 1B + 元件基址 2B + 元件数量 2B + CRC 2B
也即请求帧的前半段。
如果出错,分情况:如果CRC校验错误等无法接收则不响应,主站将超时。如果能接收但不能如约响应,则功能码最高位置1并后接异常信息1B。这个可以查资料。
正常功能码最高位是0,也即不会超过0x80。
------------------------------------------
元件基址问题:
一般有两种基址预定义,一种是以1为基址,比如西门子PLC就是如此;一种是通用以0为基址。
基址只涉及数据地址解析,其实一般定义通讯所用元件基址为以0为基址,用户外部程序应该根据实际从站的基址定义来使用元件基址。
其实基址只对设计通讯程序有效,对于外部程序来说并不需要知道。
外部只需要知道:元件类型及地址、数量、读或写、写的数据来源及读的数据存在哪里即可。
比如:对于很多PLC、变频器来说,4xxxx表示保持寄存器和地址(xxxx),这样就决定了可以使用的功能有哪些以及元件基址(xxxx-1)。
如果需要对40005这个元件设置连续5个的值,则:
功能码:0x10
元件基址:0004
元件数量:0005
数据长度:000A
还需要指定数据地址指针来填充后续数据域。
帧为:MB_Addr + 10 00 04 00 05 00 0A 数据 N*2  CRC_L CRC_H
从站正常响应:MB_Addr+10 00 04 00 05 CRC


回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-23 06:58:56 | 显示全部楼层
元件基址问题再讨论:
经过测试发现了以前对于基址问题认识的不足。
其实基址从1还是从0开始完全取决于从站对元件地址如何解析。所谓PLC Addr Base 1,指的是从站对于请求的元件地址解析为Addr -1,而从站元件地址是从0开始的,这样40001就解析为HoldReg0,40002解析为HoldReg1......。请求的元件地址必须从40001开始,不能是40000。Base0又叫协议地址,是默认解析方式。
所以,对于一个具体的RTU设备,必须先清楚通讯的元件地址和设备自身响应的元件地址的对应关系才能正确发出请求元件地址。
目前还没有碰到从站本身的元件资源地址从1开始的情况。
这些都是应用问题,不在Modbus-RTU主站开发中涉及,但对于一个具体的从站却需要首先解决:从站对于请求的地址如何和本站响应的元件地址进行对应,是Base1还是Base0。
我遇到的都是Base1。
还有需要顺便说明的是从站元件资源地址其实和元件寻址有关,比如元件寻址基于字节,但元件是2byte单元,连续元件的地址就不连续。比如西门子PLCword单元是偶数开始的地址:0、2、4......,对应协议地址就是40001、40002、40003。如果从站元件寻址基于字,连续元件地址就是连续的,元件0、1、2对应协议地址是40001、40002、40003.

回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-24 07:44:50 | 显示全部楼层
昨天调试了0x10功能,基本成型了。
通讯中的t1.5和t3.5改成使用同一个定时器TIM6,因为t1.5和t3.5不会同时使用,总是t1.5中断后启动t3.5。
TIM7作为应答超时定时器使用,定时500ms,初始化改成10kHz,0.1ms一个计数。
调试心得:使用USART1作为调试信息输出口用,有需要输出调试信息就使用printf()函数。不过也遇到一个问题,我的keil编码是UTF-8,这中文不能在XCOM中显示,只好用记事本改为GB18030编码。如果需要用串口1输出数据帧信息,需要换成16进制显示,和文本一起混显,不能很容易看清数据帧,就在帧前输出多个\n\n,以便区分。
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-24 15:47:30 | 显示全部楼层
Modbus-RTU中,功能0x03是读取连续多个保持寄存器,而功能0x04是读取连续多个AI(输入存储器),其实这两个功能在主站实现上是一样的。
我在想,如果从站不支持0x04功能,完全可以使用0x03功能,只要把AI值转入4xxxx元件即可。
目前实现了0x10、0x03、0x04功能,基本16位数据的读写功能实现了,接着就是位数据的读写。
回复 支持 反对

使用道具 举报

  离线 

24

主题

1180

帖子

2

精华

金牌会员

Rank: 6Rank: 6

积分
1962
金钱
1962
注册时间
2018-5-11
在线时间
369 小时
发表于 2018-7-24 17:32:46 | 显示全部楼层
不错,学习了。
https://github.com/ShuifaHe/STM32.git
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-25 08:26:12 | 显示全部楼层
昨天完成了基本的主站通讯试验。
实现了0x01/0x02/0x03/0x04/0x10/0x0F功能。
唯一遗憾的是我的试验套件不能设置奇偶校验,只能设置成无校验。
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-25 15:11:42 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-7-25 15:13 编辑

Modbus-RTU主站通讯控制函数:
[C] 纯文本查看 复制代码
//Modbus主站通讯控制
//Mb_Addr:从站地址
//bMode:模式0读或1写
//DataAddr:所需从站数据地址
//Num:数据byte长度,可能是读取的元件数量,也可能是写入的元件数量。
//pTr:从站回传数据缓冲区或待发送的赋值数据。
//Data_Addr:元件基址,含元件类型。
void Modbus_Master(uint8_t MbAddr, bool bMode, uint16_t DataAddr, uint16_t Num, uint8_t* pTr)
{
    //判断元件类型:HoldReg,标识为4xxxx.
    if(DataAddr >= 40000 && DataAddr < 50000)
    {
        //写操作:连续写多个HoldReg,Func=0x10
        if(bMode)
        {
            MdRTUFunc_0x10(MbAddr, DataAddr, Num, pTr);
        }
        //读操作:连续写多个HoldReg,Func=0x03
        else{
            MdRTUFunc_0x03(MbAddr, DataAddr, Num, pTr);
        }
    }

    //判断元件类型:AI,标识为3xxxx.
    if(DataAddr >= 30000 && DataAddr < 40000)
    {
        //3xxxx只支持读操作
        if(!bMode)
        {
            MdRTUFunc_0x04(MbAddr, DataAddr, Num, pTr);
        }
    }

    //判断元件类型:DI,标识为1xxxx.
    if(DataAddr >= 10000 && DataAddr < 20000)
    {
        //1xxxx只支持读操作
        if(!bMode)
        {
            MdRTUFunc_0x02(MbAddr, DataAddr, Num, pTr);
        }
    }

    //判断元件类型:DQ,标识为0xxxx.
    if(DataAddr >= 0 && DataAddr < 9999)
    {
        //强制DQ
        if(bMode)
        {
            MdRTUFunc_0x0F(MbAddr, DataAddr, Num, pTr);
        }
        //读DQ
        else{
            MdRTUFunc_0x01(MbAddr, DataAddr, Num, pTr);
        }
    }




}

回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-25 15:15:24 | 显示全部楼层
功能0x10主站实现:
[C] 纯文本查看 复制代码
//Function:0x10
//写多个连续的保持寄存器。
void MdRTUFunc_0x10(uint8_t MbAddr, uint16_t DataAddr, uint16_t Num, uint8_t* pTr)
{
    //元件基址 4xxxx
    uint16_t  Addr = DataAddr - 40000;

    //发送数据打包为数据帧。
    EnCode_0x10(MbAddr, Addr, Num, pTr);
    //发送
    SendFrame(&TX_Struct);
    //发送后转入接收。
    ReceiveFrame(&RX_Struct);

    //应答超时监测使能。
    RespTimeOut_Enb();
    //等待接收:转入接收不一定通讯接口忙,所以还必须或上是否可读。
    while((Modbus_Status_Struct.bBusy || !Modbus_Status_Struct.bFrame_ReadEnb) && !Modbus_Status_Struct.bResponse_TimeOut);
    //test
    if(Modbus_Status_Struct.bResponse_TimeOut)
        printf("F0x10接收超时!\n");

    //接收后解析
    //如果应答超时则不解析,处理下个事务(重发或进行下一帧发送)。
    if(Modbus_Status_Struct.bFrame_ReadEnb && !Modbus_Status_Struct.bResponse_TimeOut)
    {
        //如果无错则对帧进行解码。
        if(!Modbus_Status_Struct.bErr)
        {
            //如果返回数据不符。
            if(!UnCode_0x10(&RX_Struct, MbAddr))
            {
                //test
                printf("F0x10返回帧含出错信息或不是所需从站返回帧 \n\n\n\n");
                Usart_SendFrame(USART1, RX_Struct.Buffer,RX_Struct.u16Index);
                printf("\n");
            }
            //返回正确。
            else{
            //置位完成。写操作不需要转储返回的其他数据。
                Modbus_Status_Struct.bDone = TRUE;
            }
        }
        //如果出错:CRC校验失败。
        else
            printf("F0x10返回帧CRC16校验失败!\n");
    }
}

//编码0x10
//打包后的结果存放在TX_Struct中。
//pTr指向赋值字节数组。
bool EnCode_0x10(uint8_t MbAddr, uint16_t DataAddr, uint16_t Num, uint8_t* pTr)
{
    uint16_t CRC16;
    uint16_t j;
    if(Num > 123) return FALSE;  //元件数量不能大于123.

    //发送缓冲区清零
    TX_Struct.u16Index = 0;
    //从站地址
    TX_Struct.Buffer[0] = MbAddr;
    //功能码
    TX_Struct.Buffer[1] = 0x10;
    //元件基址
    TX_Struct.Buffer[2] = DataAddr >> 8;
    TX_Struct.Buffer[3] = DataAddr;
    //元件数量
    TX_Struct.Buffer[4] = Num >> 8; //高字节
    TX_Struct.Buffer[5] = Num;  //低字节
    //数据长度:Num*2
    TX_Struct.Buffer[6] = Num << 1;
    //数据域,长度取决于数量Num.
    for(int i = 0; i < TX_Struct.Buffer[6]; i++)
    {
        TX_Struct.Buffer[7 + i] = *(pTr + i);
    }

    //数据CRC生成
    //j是CRC存放开始单元,低字节在前。
    j = TX_Struct.Buffer[6] + 7;
    //生成的CRC16高字节在前。
    CRC16 = CRC16Gen(TX_Struct.Buffer, j);
    //添加CRC16,低字节在前
    TX_Struct.Buffer[j] = CRC16;
    TX_Struct.Buffer[j+1] = CRC16 >> 8;
    //帧长度字节数。
    TX_Struct.u16Index = j + 2;
    return TRUE;
}

//接收数据解析0x10
//Mb_Addr是从站站号。
bool UnCode_0x10(ModRTU_RX_Struct * pRX_Struct, uint8_t MbAddr)
{

    //如果接收到的功能码高位不为1,则接收正常。
    if(!(pRX_Struct->Buffer[1] & 0x80))
    {
        //省略比较返回的其他数据:功能码、元件基址、元件数量。
        //从站号正确
        if(pRX_Struct->Buffer[0] == MbAddr)
            return TRUE;
        else
            return FALSE;
    }
    //如果从站没有返回则会超时.
    //如果从站异常。
    else{
        return FALSE;
    }
}

回复 支持 反对

使用道具 举报

  离线 

4

主题

35

帖子

0

精华

初级会员

Rank: 2

积分
79
金钱
79
注册时间
2018-7-21
在线时间
15 小时
发表于 2018-7-25 15:35:59 | 显示全部楼层
眼,楼主厉害。
回复 支持 反对

使用道具 举报

  离线 

24

主题

1180

帖子

2

精华

金牌会员

Rank: 6Rank: 6

积分
1962
金钱
1962
注册时间
2018-5-11
在线时间
369 小时
发表于 2018-7-25 17:58:36 | 显示全部楼层
坚持就是胜利
https://github.com/ShuifaHe/STM32.git
回复 支持 反对

使用道具 举报

  离线 

6

主题

25

帖子

0

精华

初级会员

Rank: 2

积分
67
金钱
67
注册时间
2018-7-14
在线时间
15 小时
发表于 2018-7-26 00:12:27 | 显示全部楼层
xiatianyun 发表于 2018-6-24 11:46
学习中遇到了各种C语言的一些语法需要学习,很长时间没有使用C语言了,这些是遇到的坑。
1、keil C居然不 ...

keil5已经支持for(int i=0;.....)了
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-26 07:26:30 | 显示全部楼层
星邻 发表于 2018-7-26 00:12
keil5已经支持for(int i=0;.....)了

对,不过需要设置为支持c99.
回复 支持 反对

使用道具 举报

  离线 

0

主题

12

帖子

0

精华

初级会员

Rank: 2

积分
185
金钱
185
注册时间
2018-7-26
在线时间
17 小时
发表于 2018-7-26 15:17:27 | 显示全部楼层
插个眼,佩服楼主
回复 支持 反对

使用道具 举报

  离线 

0

主题

54

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
227
金钱
227
注册时间
2016-3-17
在线时间
38 小时
发表于 2018-7-28 10:58:11 | 显示全部楼层
不错,精神可嘉
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-29 07:32:18 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-7-29 07:33 编辑

从站需要做些比主站多些的工作。
具体是离散量的读写。
功能0x01是读DQ,也就是0xxxx的元件数据值。离散量的值用字节的位来表示,地址从字节的0bit到7bit,然后连续到下个字节的0bit......
在写完0x01和0x0F和0x02后才发现我没有对位信号进行处理,费了些时间来做。
关键是如何定位元件基址:在哪个字节和在字节中的哪个bit。又如何组合成数据(0x01和0x02)以及如何把数据赋给DQ(0x02)。
解决了0x01后其他两个的实现就比较简单了。
这是0x01的实现:
[C] 纯文本查看 复制代码
//Function:0x01
//读取多个连续的DQ,0xxxx
//执行函数时从站地址已经相符。
void SlaveFunc_0x01(void)
{
    uint16_t u16Num;  //元件数量
    uint16_t u16DataAddr;  //元件基址
    uint16_t u16CRC;
    uint16_t j; //CRC装载单元索引。
    uint16_t u16ByteIndex;  //基址所在字节索引,从0开始的字节索引。
    uint8_t u8BitIndex;  //基址所在字节的开始位索引,从0开始的位索引。

    //获取元件基址及数量。
    //元件基址在第2、3字节,高字节在前。
    u16DataAddr = RX_Struct.Buffer[2];
    u16DataAddr = u16DataAddr << 8;
    u16DataAddr |= RX_Struct.Buffer[3];
    //数量在第4、5字节,高字节在前
    u16Num = RX_Struct.Buffer[4];
    u16Num = u16Num << 8;
    u16Num |= RX_Struct.Buffer[5];

    //取得实际元件基址
    //如果元件地址基于1但传入的元件基址为0,则减一成为最大值0xFFFF.
    if(ADDR_BASE1)
        u16DataAddr--;

    //装配数据:
    //从站地址
    TX_Struct.Buffer[0] = MBSLAVE_ADDR;
    //功能码
    TX_Struct.Buffer[1] = 0x01;

    //判断元件数量是否合理。
    //不能超过2000(0x07D0)个DQ请求。
    if((u16Num >= 0x0001) && (u16Num <= 0x07D0))
    {
        //判断元件基址和数量是否合适。
        //从站单元地址从0开始。(可以修改为1开始,可视情况定。)
        if((u16DataAddr + u16Num) <= (DATA_MAXLEN * 8))
        {
            uint8_t u8ByteNum;  //所需字节数量

            //打包数据帧
            //响应的数据字节数量
            u8ByteNum = (u16Num%8)? (u16Num/8+1):u16Num/8;
            TX_Struct.Buffer[2] = u8ByteNum;

            //定位u16DataAddr所在字节索引。
            u16ByteIndex = u16DataAddr/8;
            //定位所在字节的开始位索引。
            u8BitIndex = u16DataAddr%8;

            uint16_t u16DQ_Index = u16ByteIndex;  //DQ字节开始索引。
            uint8_t u8DQBit_Index = u8BitIndex;  //DQ字节位索引初始值。
            uint8_t u8DQMask = 0x01 << u8DQBit_Index;  //DQ字节初始掩码.
            uint16_t u16Tx_Index = 0;  //发送字节索引初始值。
            uint8_t u8TxBit_Index = 0; //发送字节位索引初始值.
            uint8_t u8TxMask = 0x01;  //TX字节初始掩码。

            //把需要响应的数据值赋值给TX_Struct.Buffer[3]开始的单元。
            for(uint16_t i = 0; i < u16Num; i++)
            {
                if(u8DQBit_Index >= 8)
                {
                    u16DQ_Index++;
                    u8DQBit_Index = 0;
                    u8DQMask = 0x01;
                }

                if(u8TxBit_Index >= 8)
                {
                    u16Tx_Index++;
                    u8TxBit_Index = 0;
                    u8TxMask = 0x01;
                }

                if(DQ_0xxxx.u8Data[u16DQ_Index] & u8DQMask)
                    TX_Struct.Buffer[u16Tx_Index + 3] |= u8TxMask;
                else
                    TX_Struct.Buffer[u16Tx_Index + 3] &= (u8TxMask ^ 0xFF);
                u8DQMask <<= 1;
                u8TxMask <<= 1;

                u8DQBit_Index++;
                u8TxBit_Index++;
                //TX_Struct.Buffer[3 + i] = DQ_0xxxx.u8Data[i + u16ByteIndex];
            }
            //最后一个TX数据字节高位填充0
            for(; u8TxBit_Index < 8; u8TxBit_Index++)
            {
                TX_Struct.Buffer[u16Tx_Index + 3] &= (u8TxMask ^ 0xFF);
                u8TxMask <<= 1;
            }

            j = u8ByteNum + 3; //j为CRC所在单元。

        }
        else{
            //产生异常02:地址非法
            TX_Struct.Buffer[1] |= 0x80;
            TX_Struct.Buffer[2] = 0x02;
            j = 3;
        }
    }
    else{
        //产生异常03:数据非法
        TX_Struct.Buffer[1] |= 0x80;
        TX_Struct.Buffer[2] = 0x03;
        j = 3;
    }

    //生成CRC16。
    u16CRC = CRC16Gen(TX_Struct.Buffer, j);
    TX_Struct.Buffer[j] = u16CRC;
    TX_Struct.Buffer[j+1] = u16CRC >> 8;
    //帧长度字节数。
    TX_Struct.u16Index = j + 2;
    //发送打包后的帧
    SendFrame(&TX_Struct);
}
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-29 22:55:29 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-7-29 22:59 编辑

从站设计重点:
当出现CRC16校验错误时,必须重启接收:清空接收区并使能接收。
为什么会出现CRC校验错误?
除了传输链路受到干扰产生错误外,很大一部分来源于主从配合。
如果主从都是一个设计开发,那么可以做到对主站是如何轮询通信事务比较了解,如果主站不是同一个设计开发的,那么主站如何轮询就不是很清楚了。
主站如何轮询的?
主要有两种:一种是定时请求,以一定的时间间隔发送请求,多个不同的请求排序发送,这个需要有一个时间间隔的相对参考,可以是同一个参考也可以是上个请求结束后为参考点。如果是同一个时间参考点,需要设计好各个通信事务能够被先后排序。
另一种是轮询排序,在上个请求没有结束前不能进行下个请求。这个我以前使用都是这样的方式。
而从站是被动接收请求的,如果上个请求能够及时处理完毕那么下个请求也能个及时接收。这就要求主站请求间隔足够长。如果上个请求没有处理完主站就发出了下个请求,这样当从站转入接收时会收到不完全的数据,这就导致了校验错误。
发生CRC错误时如果没有清空缓冲区下个请求数据会接着上次的末尾,导致了再次校验错误。所以,发生CRC错误时必须先清空缓冲区接收下个请求。
这点是设计时被我忽略的地方。
常常发生在主站有4-5个请求,各个请求时间间隔不定时。
我使用的是Modbus-poll模拟软件做测试,没有相关说明,只能看出各个请求使用的是间隔扫描方式,也就是定时轮询。这就有了冲突,也给测试带来了挑战。
同理,主站模式下如果发生了接收CRC错误也应该重启发生。当然,主站采用的是超时监测。
无论如何,当发生错误时都应该复位通信,重启通信。
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-31 07:08:27 | 显示全部楼层
昨天在论坛看到有关于按键操作的好示例,我把以前的按键扫描程序重新设计了下:
[C] 纯文本查看 复制代码
//key0 扫描程序
//bSusKey==TRUE:连续按下可以扫描到连续多个值。
//bSusKey==FALSE:连续按下只扫描到一个按键值。
bool key0_Scan( bool bSusKey )
{
    static bool bsLastKey = FALSE;  //按键上次状态。
    //经过防抖处理后检测到按键压下。
    bool bKey;
    //防抖动检测定时器。
    static TimerType timer;
    //返回值。
    bool bRet = FALSE;

    assert_param(Is_BOOL(bSusKey));

    // 防抖处理。
    bKey = TimeON(KEY0_CODE, 150U, &timer);
    bRet = bKey & (bKey ^ bsLastKey);  //沿检测。上下沿均为TRUE,非沿时为FALSE.
    //如果是连续按键模式。
    if(bSusKey)
        bsLastKey = FALSE;  //当是连续模式时,把上次按键状态置为FALSE。
    //如果是单按键模式。
    else
        bsLastKey = bKey;


    return bRet;
}

//key1 扫描程序
//bSusKey==TRUE:连续按下可以扫描到连续多个值。
//bSusKey==FALSE:连续按下只扫描到一个按键值。
bool key1_Scan( bool bSusKey )
{
    static bool bsLastKey = FALSE;  //按键上次状态。
    //经过防抖处理后检测到按键压下。
    bool bKey;
    //防抖动检测定时器。
    static TimerType timer;
    //返回值。
    bool bRet = FALSE;

    assert_param(Is_BOOL(bSusKey));

    // 防抖处理。
    bKey = TimeON(KEY1_CODE, 150U, &timer);
    bRet = bKey & (bKey ^ bsLastKey);  //沿检测。上下沿均为TRUE,非沿时为FALSE.
    //如果是连续按键模式。
    if(bSusKey)
        bsLastKey = FALSE;  //当是连续模式时,把上次按键状态置为FALSE。
    //如果是单按键模式。
    else
        bsLastKey = bKey;

    return bRet;
}


//WKUP 扫描程序
//bSusKey==TRUE:连续按下可以扫描到连续多个值。
//bSusKey==FALSE:连续按下只扫描到一个按键值。
bool WKUP_Scan( bool bSusKey )
{
    static bool bsLastKey = FALSE;  //按键上次状态。
    //经过防抖处理后检测到按键压下。
    bool bKey;
    //防抖动检测定时器。
    static TimerType timer;
    //返回值。
    bool bRet = FALSE;

    assert_param(Is_BOOL(bSusKey));

    // 防抖处理。
    bKey = TimeON(WKUP_CODE, 150U, &timer);
    bRet = bKey & (bKey ^ bsLastKey);  //沿检测。上下沿均为TRUE,非沿时为FALSE.
    //如果是连续按键模式。
    if(bSusKey)
        bsLastKey = FALSE;  //当是连续模式时,把上次按键状态置为FALSE。
    //如果是单按键模式。
    else
        bsLastKey = bKey;

    return bRet;
}


回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-7-31 07:17:00 | 显示全部楼层
Modbus-RTU从站通信仍然有问题,当主站存在太多需要通信的事务时就会在时间累积长时出现接收错误,主站出现超时。
回复 支持 反对

使用道具 举报

  离线 

76

主题

732

帖子

0

精华

金牌会员

Rank: 6Rank: 6

积分
1301
金钱
1301
注册时间
2014-3-7
在线时间
266 小时
发表于 2018-8-2 14:42:24 | 显示全部楼层
谢谢分享
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-3 07:08:06 | 显示全部楼层
昨晚上传的帖子怎么消失了,可惜了。
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-3 07:08:32 | 显示全部楼层
这几天用几台RTU设备测试我的Modbus-RTU通讯程序,加深对RTU通讯的了解。
几点体会:
1、其实相对来说主站程序容易些。
2、t1.5定时可以取消不用,只用t3.5监测帧结束。
3、t1.5和t3.5的定时时间其实并不是固定不变的,我一开始按照固定的来处理只适合19200以上的波特率,低于次波特率需要根据波特率来计算。
具体是:t3.5是发送3.5个字节所需时间,一个字节有11bit需要发送,1bit需要一个波特率周期,则一个字节所需时间是:11/BoundRate *1000000 us。t3.5=3.5*11/BoundRate *1000000 us.
4、接收到一个字节就复位t3.5并重启定时,如果出现通讯异常不能正常接收到该字节时不存储该字节但需要继续接收下一字节,到CRC验证时自然失败,后续处理会丢弃该帧。
5、t3.5中断时帧结束,如果CRC验证失败需要重启接收环节:清零接收缓冲区。
6、CRC验证Ok,需要把RS485端口设为发送,屏蔽接收,这样接下来的处理不会再接收新数据到缓冲区。如果不设为发送,由于接收是使用中断来转储数据到缓冲区的独立过程,会导致处理接收到的数据环节和帧结束时的缓冲区数据不一致。试验证明虽然不影响处理,但确实不妥。
7、如果不是发到本站的数据,不用回复。回复会影响其他站的处理。这是由Modbus-RTU协议规定的,我们不能乱来。
8、按理说每个从站都会接收到主站发出的数据,但试验证实并非如此。由于各个从站接收数据的时间不是一致的(由硬件不同和线路长度不同决定),有些发到其他从站的帧不能被接收到,这个这是试验得出的结果,具体原因还不清楚。
9、8中说的不能被接收到,其实也不准确,是经过接收处理后会出现自动屏蔽掉一些帧。我的试验是发到紧邻的从站的帧不能接收到,而发到上上一个从站的帧却可以接收到。
10、关于数据类型定义,如果不是使用硬件数据,还是使用原生的比较合适,比如uint8_t比使用u8合适,


回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-5 19:44:34 | 显示全部楼层
学习完Modbus-RTU通讯,基本达到了我原来预定的学习目的。
在学习Modbus的过程中我对开发模板做了调整。学习中发现我需要对原来的版本进行改进,这样就需要版本控制的支持,我选择了git作为版本控制工具。
为了配合git,需要把一些不是代码的部分从开发目录中剥离,而把纯代码部分独立做成一个工作目录Code。
接着把原来Output和List还是设置到project目录,Project目录存放MDK项目,也就是.uvprojx文件。
把Code作为git库的工作区进行开发。
这样git管理的就是纯代码部分了。
-----------------------------------------
如果时间精力允许,我还会继续学习STM32知识。
比如RTC实时时钟,输入捕获,ADC和DAC模数转换。
至于图形图像多媒体部分,我觉得这不是STM32的强项。
只需要简单地学习触摸屏操作,实现信息显示就可以了。


回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-8 06:55:26 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-8-8 07:35 编辑

来学习RTC实时时钟,做个多功能的时钟,只是我没有液晶显示屏。
目标:通过串口和威纶通触摸屏通讯实现实时时钟的校准,然后用串口提取当前时间和设置闹钟。
刚学习RTC,发现如何设置RTC的当前时间是个麻烦的问题:不能时时校准同步当前时间。如果程序中设置了一个当前时间的话等下载到开发板中去时其实已经不是当前时间了。
如果没有联网实现网络校准的话如何来实现呢?
我想到了以前用的威纶通触摸屏模拟软件有这个功能,威纶通可以获取PC上的当前时间并通过Modbus-RTU和RTU设备通讯,我就利用这点来给精英板校准吧。

或者直接和西门子PLC通讯,取得当前时间的秒值,直接赋值给精英版CNT。西门子当前时间也是从1970年某个时刻(1970-1-1-0:0)开始的秒值,可以精确到ns级,我只需取s值。

回复 支持 反对

使用道具 举报

  离线 

5

主题

159

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
339
金钱
339
注册时间
2018-7-19
在线时间
76 小时
发表于 2018-8-8 16:39:48 | 显示全部楼层
a496298685 发表于 2018-6-27 22:55
您刚才说到,“外部中断不仅仅只是GPIO产生的中断”,那么还有什么是外部中断呢?

PVD   RTC   USB
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-11 10:33:44 | 显示全部楼层
学习RTC中发现需要学习的知识远不止RTC本身,需要学习后备寄存器操作、电源管理等等。其实RTC本身并不特殊,工作基本原理可以看成普通的定时器。
在设置当前时间环节需要一些关于时间的储备知识。
日期和时间是两个不同的概念,日期是相对定义的,公历日期是指格里历。公元1年有而公元0年不存在,所以年份是从1开始的。
可以用秒来取代日历时间,所以日历时间一般指用秒数来表示的日期时间。这就很大的数字了,不过放在数学上也不是太大。
计算机等计算设备里面一般有一个存储日历时间的储存器,用来表示从某个日期时间开始的累积秒数,这个就是日历时间。
作为锚点的时间不是太统一,大多数是用1970-1-1-0:0:0来作为参照。也就是说日历时间是距离这个时间的累积秒数。
要把秒数变换成看得懂的日期时间需要进行时区变换。不过这个也不是一定需要做的。
如果设备不和其他时区的设备进行交互,大可以不作时区换算。
计算机以前大多需要支持飞来飞去的使用场景,所以就有了UTC和本地时间的变换。
现在由于都互联了,所以直接使用网络对时,不需要人工参与了。
不过我们使用的计算机储存的秒数是可以手工设置成两种不同的秒数模式的:是储存本地时间秒数还是储存UTC时间秒数。
如果储存本地秒数,读出来转换后就不要再转换为本地时间了。
在windows和Linux双系统下可能需要做一些设置才能统一,因为这两个系统对日历时间是本地还是UTC的默认模式不同。
我用来试验的STM32不用这么处理,一律作为本地时间看待。
---------------------------------------------------------



回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-11 10:53:53 | 显示全部楼层
根据给定的日期时间转换为日历秒数。
首先需要计算给定日期一共有多少天?
这个还真是费了些时间。
由于地球公转一周需要的天数是365.2423天,每4年就会比365天/年多出来0.9692天,就有了闰年的措施。
如果4年一闰,400年时间内,实际应该闰的天数应为96.92天,可却闰了100天。多出来3天多点。
于是再规定逢100不闰。
这样400年就不闰了4天,可实际只需要不闰3天多点,于是再规定逢400年就再闰一年。
这样,判断闰年的算法就是:年份可以被4整除且不能被100整除,或者可以被400整除。
误差多少呢?400年的误差为97-96.92=0.08天。400*100年多出来8天,5000年多出来1天。
不过5000也在上述算法以内有效,10000就无效了,所以我规定年份不能被5000整除。哈哈
不过,人类为什么要这样呢?一周是365.2423天,倒推秒数,延长些不就解决了嘛。
工匠不干了。



回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-11 11:43:49 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-8-11 11:46 编辑

这个函数是我的验证算法:

[C] 纯文本查看 复制代码
const int mon1_table[12]={0,31,59,90,120,151,181,212,243,273,304,334};
int dt2Sec(int iYear, int iMon, int iDay, int iHour, int iMin, int iSec)
{
    int iDay1,iYear1;
    int iSec1;
    iYear1 = iYear - 1;
    iDay1= ((iYear1/4 - iYear1/100 + iYear1/400) + iYear1*365);

    int iMon1 = iMon - 1;
    iDay1 = iDay1 + (mon1_table[iMon1]);
    //如果是闰年且是2月以后月份,天数加1.
    if(Is_LeapYear(iYear) && (iMon1 >= 2))
        iDay1++;

    //天数加上当月内日期之前的天数。
    iDay1 += (iDay - 1);
    iDay1 -= 719162;

    //计算秒数。
    iSec1 = ((((iDay1 *24 + iHour) * 60) + iMin) * 60) + iSec;
    return iSec1;
}


函数中的719162是1970年前的所用天数,这个是通过函数内的iDay1算法得到的常数。

回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-12 22:40:38 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-8-13 15:35 编辑

来看看32位的RTC计数器可以计时到什么时候。
32为无符号整数最大值为4294967295,这么多秒大约可以表示136年的时间,当然有闰年的存在,不能很确切地说具体到什么时候,不过如何从1970年算起的话应该有个确定的时间。
具体我试了下,到2106年出现的错误,也就是超出了32位无符号整数范围了。
那就保守点社会调查2105年12月31日吧,这个没有问题。
把一个日期时间换算成秒还比较简单,如果把一个秒数日历时间具体转换为日期时间就比较麻烦了。
采用试算拟合的方法,比较费时间。

[C] 纯文本查看 复制代码
//日历时间(秒数)转换为日期时间
//锚点是1970-1-1-0:0:0
//不作时区转换,一律看作本地时间。
void Sec2Dt(uint32_t u32Sec, dtStruct* dt)
{
    uint16_t u16Day, u16Mon;
    uint32_t u32Sec_Day;  //不足一天的秒数。
    uint16_t u16Year = 1970;

    //计算整天数。
    u16Day =(uint16_t)(u32Sec / 86400);
    //计算不足一天的秒数。
    u32Sec_Day = u32Sec % 86400;

    //计算年份.
    while(u16Day >= 365)
    {
        //如果是闰年
        if(Is_LeapYear(u16Year))
        {
            if(u16Day >= 366)
                u16Day -= 366;
            else
                break;  //剩余天数为365且是闰年,结束年份拟合。
        }
        //不是闰年
        else{
            u16Day -= 365;
        }
        u16Year++;
    }
    //年份赋值
    dt->u16Year = u16Year;

    //计算月份.
    //u16Day为一年内的天数.
    u16Mon = 1;
    bool bIsLeap = Is_LeapYear(u16Year);
    while((u16Mon <= 11) && (u16Day >= ((bIsLeap && (u16Mon >= 2))? mon1_table[u16Mon]+1 : mon1_table[u16Mon])))
    {
        u16Mon++;
    }
    //要么u16Mon是12,要么中途拟合上某个月份。
    //月份赋值
    dt->u16Mon = u16Mon;

    //计算月内天数。
    //计算当天之前的月内天数。
    //如果月份小于等于2 或 非闰年。
    if(dt->u16Mon <= 2 || !bIsLeap)
        u16Day -=  mon1_table[dt->u16Mon-1];
    //如果月份大于2 且 是闰年。
    else
        u16Day -= (mon1_table[dt->u16Mon-1] +1);

    //加上当天。
    u16Day++;
    //日期赋值
    dt->u16Day = u16Day;

    //计算时间
    //u32Sec_Day是不足一天的秒数。
    dt->u16Hour = (int)(u32Sec_Day / 3600);
    dt->u16Min =  (int)((u32Sec_Day % 3600) / 60);
    dt->u16Sec =  (int)((u32Sec_Day % 3600) % 60);
}


经过大量从1970-1-1到2105-12-31的某个时间点的循环验证,没有问题。
--------------------
附记:原子提供的程序中,计算年份的地方有些问题,当年份是闰年且天数不足366天时,程序把年份+1后退出年份拟合,这是错误的,不应该年份+1了。


回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-14 15:31:08 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-8-14 15:32 编辑

学习RTC总是一头雾水,原因可能是因为涉及到要同时处理PWR、BKP、RTC三个不同类型的寄存器。
1、RTC所使用的时钟一般是LSE,也就是外部低速时钟。要使用就需要配置时钟。
以前只知道要使用外设只需要使能某个总线上外部就可以了,现在还需要配置时钟,为什么以前不需要呢?这个还真是不清楚。
唯一的可能是其他时钟在系统初始化阶段就已经初始化了,不需要我们配置。
为什么系统初始化时不一起配置这里要用到的LSE呢?不知道。
2、如何配置?
LSE的配置需要使用PWR和BKP。
LSE有两种时钟源可以选择:外部晶振和外部时钟源。这两个接法不同。这个用RCC_BDCR中的LSEON、LSEBYP来设置。
RTC时钟是使用LSE还是LSI、还是HSE的128分频也需要使用RCC_BDCR中的RTCSEL来配置。
RTC时钟选择后需要使能才能正式工作,这个也需要使用RCC_BDCR中的RTCEN来使能。
----->RCC_BDCR中的这几个位是位于后备寄存器域,可我在BKP寄存器映像图中并没有找到这几个位的映像。
这样就需要对后备寄存器域进行操作。
而后备寄存器和RTC有个共同特点,就是系统复位后这两种寄存器处于写保护状态,需要通过PWR_CR中的DBP为来置位设置才能允许写操作。
这样就需要对PWR进行访问。
3、逻辑就是这样的,环环相扣。
所以对RTC访问需要做以下操作:
1、使能APB1总线上的PWR后BKP,以便对PWR和BKP进行操作。
2、置位PWR_CR.DBP,以便允许写操作BKP和RTC。
-----------------------------
也即:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);


回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-14 16:01:29 | 显示全部楼层
接着设置LSE使用晶振还是外部时钟进行设置,是对RCC_BDCR.LSEON和LSEBYP进行操作。
RCC_LSEConfig(RCC_LSE_ON);
由于需要检测设置是否使外部晶振或外部时钟源工作进行检测(检测RCC_CR、RCC_BDCR、RCC_CSR等寄存器的标识位),所以还不需等待检测到才意味着LSE起振了。
由于外部设备(真正的片外设备)工作需要一个时间,所以需要足够的时间。如果一定时间内还是检测不到自然就失败了。例程给的时间是2.5s。
检测到LSE起振后才能设置RTC时钟源为LSE。
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);

当然还是通过设置RCC_BDCR的RTCSEL位和RTCEN位来实现。
需要检测RTC寄存器的写操作是否完成,所以需要RTC_WaitForLastTast()函数,这个主要检测RTC_CRL的RTOFF位是否为1。
不过有个问题,之前操作的都是RCC相关寄存器,并没有操作RTC寄存器,为何需要这个检测呢?
接下来检测RTC同步。
由于采用的时钟不同,如果需要操作RTC寄存器就需要让时钟同步才能从内核总线读写RTC这个不同的时钟设备。
同步当然不是让它们一致的意思。
查手册发现是等待RTC的三个重要寄存器同步,CNT、PRL、和ALR。依样画葫芦吧。

函数是:RTC_WaitForSynchro();同步一次即可。



回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-14 16:22:55 | 显示全部楼层
经过上述操作,RTC可以工作了。
接着对RTC三个重要寄存器进行操作。
写操作需要使能,这个在前面已经做过了,不必再做了。
不过对这三个寄存器写前需要从另一个位来允许,写完想要有效也需要这个位来允许:RTC_CRL.CNF置位允许写入,复位才使写入的数据生效。
通过函数:RTC_EnterConfigMode()和RTC_ExitConfigMode()来实现。
这样设置重重防御确实厉害得不行。
不过库函数里已经为我们自带突防了,不必自己再操作。
具体是几个函数:RTC_SetCounter()和RTC_SetAlarm()。以及设置预分频的函数:RTC_SetPrescaler()。
我要实现的是用另外一个软件来把PC时时时钟数据通过Modbus发到开发板来设置RTC的CNT,就不在RTC_Init()里来设置CNT了,只需设置预分频。
闹钟暂时没有学习。
设置完RTC寄存器均需等待完成,这个前面说过了。RTC_WaitForLastTask();
---------------------------
这些操作不需要每次复位重启都执行一遍,因为这些配置数据存在后备寄存器中,停电不会消失。
为了让系统知道已经配置过了,需要一个标识。
实现方法是通过在BKP中写入一个数据,检测到这个数据证明已经设置过了,没有检测到就说明没有设置过。
类似与C头文件中的宏定义#ifndef ...#defince...#endif
无论如何接下来的设置中断是需要的,因为中断设置数据不存放在BKP中。
RTC核心只有三个寄存器:PRL/CNT和DIV。这三个像BKP一样不会掉电复位,其掉电后通过后备电池提供电源工作:LSE和RTC计数功能。




回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-14 16:30:49 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-8-14 16:34 编辑

昨天实现了通过威纶通触摸屏的仿真功能利用Modbus-RTU和开发板通讯,从威纶通软件上给精英版发送PC日期时间数据,STM32收到日期时间数据后转换为日历秒数来写入RTC的CNT,现在我的开发板已经开始作为一个真正的时钟在运行了:通过串口1发送秒中断读取的RTC时间显示在串口助手上,和电脑上的时间几乎一致(有大约2s的差异,可能是威纶通从发送到开发板上需要时间,以及串口1把数据发到助手上也需要时间有关。不过,怎么是超前2s啊!!!)。
-----------------
刚才重新同步时间,开发板迟缓1s,这是合理的。等明天再看累积情况。

回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-14 20:49:17 | 显示全部楼层
使用上位机软件把PC当前时间同步到RTC,RTC开始自动计时,由于通讯传输的延迟,观测到的RTC实时时间比PC上的时间慢了1s钟。
经过大约4小时在观察,RTC上的时间比PC上的时间快了2s钟。
我知道PC上有网络校时功能,但是我的PC已经关闭了该功能了。
是RTC时钟不准吗?我的RTC时钟采用LSE,32.768kHz,预分频系数是32767.
--------------------------
本来发的是悬赏新帖,但是我的浏览器显示不了“滑动验证”,发不了贴子,只好先发到这里了。
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-15 07:06:17 | 显示全部楼层
今天搜了下论坛,RTC定时不准确可能是晶振的问题,这个只好放弃了。
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-8-20 16:26:23 | 显示全部楼层
这几天乱七八糟的随便看,学习进度明显下降。
开发板上的RTC时钟经过大约5天多点的运行已经明显快了许多,快了有一分多钟了。不靠谱。
回复 支持 反对

使用道具 举报

  离线 

10

主题

51

帖子

1

精华

中级会员

Rank: 3Rank: 3

积分
395
金钱
395
注册时间
2017-8-24
在线时间
36 小时
发表于 2018-8-21 11:54:41 | 显示全部楼层
很好的
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-9-4 22:34:26 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-9-4 22:36 编辑

宏定义容易出错的地方:
定义了键值宏,在主程序里面用键缓冲区内的值和该宏做比较。
[C] 纯文本查看 复制代码
#define KEY0_WKUP      (KP_KEY0 + KP_WKUP) && (Keypad.u16DownTrg == KP_WKUP)
......
bIsKey0 = (Keypad.u16ValOnce == KEY1_KEY0) || (KeyVal == KEY1_DOUBLE);

这个写法没有问题。
可是如果把宏定义值部分用()包围就完全不一样了:
[C] 纯文本查看 复制代码
#define KEY0_WKUP      ((KP_KEY0 + KP_WKUP) && (Keypad.u16DownTrg == KP_WKUP))
.....
bIsKey0 = (Keypad.u16ValOnce == KEY1_KEY0) || (KeyVal == KEY1_DOUBLE); 

表达式扩展为:
bIsKey0 = (Keypad.u16ValOnce ==((KP_KEY0 + KP_WKUP) && (Keypad.u16DownTrg == KP_WKUP))) || (KeyVal == KEY1_DOUBLE);
这样,比较的顺序成了先得出一个bool值再和Keypad.u16ValOnce比较,只有数值完全一样时才会为真,这和要求不一样了,得不出所需结果。
实际所需为:先比较键值是否一致再判断是否发生了按键按下。即前一个程序扩展为:
bIsKey0 = (Keypad.u16ValOnce ==(KP_KEY0 + KP_WKUP)  &&  (Keypad.u16DownTrg == KP_WKUP)) ||  (KeyVal == KEY1_DOUBLE);
---------------
宏定义#define存在许多容易被忽视的地方,或者讲是C语言宏定义极易出错的地方。这个是因为C中的宏是替换,不一定在使用的地方以独立整体出现。


回复 支持 反对

使用道具 举报

  离线 

24

主题

1180

帖子

2

精华

金牌会员

Rank: 6Rank: 6

积分
1962
金钱
1962
注册时间
2018-5-11
在线时间
369 小时
发表于 2018-9-4 22:46:57 | 显示全部楼层
xiatianyun 发表于 2018-9-4 22:34
宏定义容易出错的地方:
定义了键值宏,在主程序里面用键缓冲区内的值和该宏做比较。
[mw_shl_code=c,tru ...

我的键扫程序在您的关心帮助下,
已经算是定型了,
在坛友的提议下,发在了https://github.com/ShuifaHe/STM32.git
有空可移步指导,赏个星星。
https://github.com/ShuifaHe/STM32.git
回复 支持 反对

使用道具 举报

  离线 

18

主题

245

帖子

1

精华

高级会员

Rank: 4

积分
697
金钱
697
注册时间
2018-4-13
在线时间
117 小时
 楼主| 发表于 2018-9-4 22:54:07 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-9-4 22:58 编辑
warship 发表于 2018-9-4 22:46
我的键扫程序在您的关心帮助下,
已经算是定型了,
在坛友的提议下,发在了https://github.com/ShuifaH ...

已经点赞了。
我是初学,谢谢你提供程序供我学习。
这是我的程序,只实现了双击、长按、键盘缓冲区的常用操作。
https://github.com/azjiao/KeyComb


回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则




关闭

必看,必学:"原子哥”力荐上一条 /1 下一条

正点原子公众号

QQ|联系我们|手机版|官方淘宝店|微信公众平台|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2018-9-22 03:29

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表
/* */