||
1、 协议,不说,文库 modbus通讯协议,有中文的
2、 环境stm32f103vc,裸奔,有系统可用openmodbus,仅做实验还可以uc/modbus,有slave源码,这两个都没试验过
3、只实现功能,不讨论效率,我自认为我的代码效率不高,我接受的底线就是不要polling
4、资源,需要一个串口,好像是废话,再需要一个定时器,用于判断帧尾,st定时器多的用不完,找个最简单的tim7吧
5、扯了好多没用的
先初始化串口,打开发送接收中断
初始化定时器,时间 = 串口发送1字节时间 x 10,不用开始工作,待命吧
额,不是 串口发送1字节时间 x 3.5吗?那是最小时间,我跟master程序员协商过了,最小10个吧,master流程,发送请求,等待应答(应答要么得到正确应答,要不不耐烦了timeout)。
代码
void modbusRTU_open(uint8_t Addr, uint16_t * pBuffer, uint16_t Length)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE);
RCC_APB2PeriphClockCmd (RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_DeInit(TIM7);
TIM_TimeBaseStructure.TIM_Period = 10000;
TIM_TimeBaseStructure.TIM_Prescaler = 36;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM7, &TIM_TimeBaseStructure);
/* Configure USART1 Rx (PA10) as input floating */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* Configure USART1 Tx (PA9) as alternate function push-pull */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_2;
USART_InitStructure.USART_Parity = USART_Parity_No ;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
USART_Cmd(USART1, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
ClintAddr = Addr;
ClintBufferLength = Length;
pClintBuffer = pBuffer;
}
6、 3个参数什么意思
Addr,节点地址
pBuffer,数据区首地址
Length,数据区长度,单位WORD 16bit
保存这几个参数到全局变量,好了,不会丢了
额,指针,数组,我没有检查指针合法和数组边界,手有点发抖,有点后怕,万一指针非法,数组越界,后果不堪设想,怎么办,用的时候小心吧
关于数据区,modbus会请求某个地址数据的读或写,这个地址和实际存储空间要一一映射好,置于数据存储是否连续没啥要求,看着处理吧,连续也许会省很大事
7、 好了,接收数据吧
先来个缓冲存储接收数据
uint8_t ReceiveCache[128];
uint8_t ReceiveCacheCount;
uint8_t ReceiveCachePointer;
缓冲大小和master沟通吧,能放下最大数据帧就好,再来个帧个数计数,不然我怎么知道接收了多少数据,还要有个offset,不然我怎么知道下一个数据该存储那个位置
缓冲区有了,可以接收了吧,接收中断代码
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
ReceiveCache[ReceiveCachePointer] = USART_ReceiveData(USART1);
ReceiveCachePointer++;
modbusRTU_TimerStart();
}
最后一句什么用,每次接收中断,重新开始计时,还记得tim7是干啥的吗。能看看里面的内容么,of coursevoid modbusRTU_TimerStart(void)
{
TIM_SetCounter(TIM7, 1); //为什么会是1而不是0,您随意,我不差1个数
TIM_ClearITPendingBit(TIM7, TIM_IT_Update);
TIM_ITConfig(TIM7, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM7, ENABLE);
}
8、 数据放到缓冲区了,怎么用,不急,还没有帧尾呢
什么时候帧尾,当然是我们的tim溢出中断,如果一直没有数据被接收,也就没有接收中断,很快它就溢出了
“不要发了,让它溢出~!”。“收到!马上停工...”
“oh,中断了”,帧尾来了,处理他吧,等等,怎么才能让别人知道帧尾呢,顶一个标识吧 全局变量
uint8_t ReceiveFrameFlag;
中断代码
if(TIM_GetITStatus(TIM7, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM7, TIM_IT_Update);
TIM_ITConfig(TIM7, TIM_IT_Update, DISABLE);
TIM_Cmd(TIM7, DISABLE);
TIM_SetCounter(TIM7, 1);
ReceiveFrameFlag = 1; //完成一帧
ReceiveCacheCount = ReceiveCachePointer; //数据帧长度
ReceiveCachePointer = 0;
}
怎么处理数据,管他呢,我只管判断帧尾
9、数据处理
void modbusRTU_main(void)
{
uint8_t modbus_cmd;
uint16_t modbus_addr;
uint16_t modbus_length;
// uint16_t modbus_crc;
if(ReceiveFrameFlag == 1)
{
ReceiveFrameFlag = 0; //告诉你们,你们的数据我处理了
//数据要在这里处理,此处省略代码若干
}
}
到底怎么处理..好吧
9.1、 判断地址,你想独享总线,那是不可能的,还记得存储在ClintAddr里面的节点地址么,比较一下
if(ReceiveCache[0] != ClintAddr)
{
return; //不是叫我,我才懒得搭理呢! 美得你,你无权
}
9.2、 数据接收正确吗,验证下吧
if(modbusRTU_CRC(ReceiveCache, ReceiveCacheCount) != 0x0000)
{
return; //数据出错,毫不客气的丢弃
}
你判断的神马?
uint16_t modbusRTU_CRC(uint8_t *pBuffer, uint16_t Length); CRC校验,问问度娘吧 CRC16
参数又是指针和长度……怎么办,我手发抖,精神紧张,程序不要崩溃啊
额,你不要吧非法的参数给我,又怎么能崩溃呢
9.3、 都对了,处理吧
modbus_cmd = ReceiveCache[1]; //第二字节是请求代码
TransmitCache[0] = ClintAddr;
TransmitCache[1] = modbus_cmd;
if(modbus_cmd!=0x03 && modbus_cmd!=0x06 && modbus_cmd!=0x10)
{
return; //我们只响应这几个,别的不管,我的master只要这几个
}
有个地方,TransmitCache[0]是什么,刚才说到接收需要缓冲区,这个是发送缓冲区,一会说吧,先把接收到的请求处理完了吧
9.3、 获取数据区地址
modbus_addr = (uint16_t)(ReceiveCache[2]<<8 | ReceiveCache[3]);
9.4、 根据请求散转
switch(modbus_cmd)
{
case 0x03:
modbus_length = (uint16_t)(ReceiveCache[4]<<8 | ReceiveCache[5]);
modbusRTU_Cmd03(modbus_addr, modbus_length); //处理03
break;
case 0x06:
modbusRTU_Cmd06(modbus_addr, &ReceiveCache[4]); //处理06
break;
case 0x10:
modbus_length = (uint16_t)(ReceiveCache[4]<<8 | ReceiveCache[5]);
modbusRTU_Cmd16(modbus_addr, modbus_length, &ReceiveCache[7]); //处理16
break;
default:
break;
}
这里面的三个处理函数
void modbusRTU_Cmd03(uint16_t Addr, uint16_t Length);
void modbusRTU_Cmd06(uint16_t Addr, uint8_t *pdata);
void modbusRTU_Cmd16(uint16_t Addr, uint16_t Length, uint8_t *pdata);
什么意思,怎么实现,看协议,看协议,需要一遍一遍的看
好吧,我贴个03的
void modbusRTU_Cmd03(uint16_t Addr, uint16_t Length)
{
uint16_t i;
if(Addr+Length >= ClintBufferLength)
{
return; //检查地址和数据长度,这个事master传来的数据,我对他不是很放心。
//其实他的水平真的很高,不是对他不放心,而是自己
}
TransmitCache[2] = Length * 2;
for(i=0; i<Length; i++)
{
TransmitCache[3+(i*2)] = pClintBuffer[i+Addr]>>8;
TransmitCache[4+(i*2)] = pClintBuffer[i+Addr]&0xFF;
}
i = modbusRTU_CRC(&TransmitCache[0],(Length*2)+3);
TransmitCacheCount = (Length*2)+5;
TransmitCache[3+(Length*2)] = i>>8;
TransmitCache[4+(Length*2)] = i&0xFF;
modbusRTU_Transmit();
}
最后一句是什么?
9.5、 回应,好,先来说说发送缓冲吧,定义
uint8_t TransmitCache[128];//长度问问你的master最长请求多么长的数据
uint8_t TransmitCacheCount;
uint8_t TransmitCachePointer;
为什么需要缓冲,我算出一个来发送一个不行么
两个问题,一、你想polling吗,我不接受;二、如果你两个字节之间间隔时间大于 一个字节时间x3.5,master可就要认为我帧尾了。所以要先放缓冲,准备好了一痛全发走
前面三个函数给我们准备好缓冲区了,连回应数据帧长度也给出了,哦还有crc也有了,真好,只需要发走就行了
那么他最后那个函数就是发送
void modbusRTU_Transmit(void)
{
USART_ClearITPendingBit(USART1, USART_IT_TXE);
USART_ITConfig(USART1, USART_IT_TXE, ENABLE); //先打开中断
TransmitCachePointer = 1; //为什么会是1而不是0
USART_SendData(USART1, TransmitCache[0]);//因为第0个在这,不然你怎么触发发送中断呢,先塞进一个数去,发送完了就有了
}
中断中还要把剩余的发送走
if(USART_GetITStatus(USART1, USART_IT_TXE) != RESET)
{
USART_ClearITPendingBit(USART1, USART_IT_TXE);
USART_SendData(USART1, TransmitCache[TransmitCachePointer]);
TransmitCachePointer++;
if(TransmitCachePointer >= TransmitCacheCount) //判断发送完成
{
TransmitFrameFlag = 1;
TransmitCachePointer = 1;
USART_ITConfig(USART1, USART_IT_TXE, DISABLE); //没有需要发送的了,再来中断怎么办,关了吧,不会来了吧
}
}
10、 还有什么,恩,有
void modbusRTU_main(void);这个什么时候调用,他可是处理数据最重要的函数啊。
我先问,需要实时吗?需要,为啥不用RTOS,稳定,实时性好。
那怎么办,尽最大努力。放到主循环,每次扫描吧,省事。我的程序需要很长时间才能运行回来,怎么办,把你所有的polling都处理掉吧,别让他们烧内核了。
我放到定时器中断里定时扫描行么,那你为什么不直接放在帧尾定时器tim7中断中直接处理了呢,那样更快!只不过中断时间变长,
有问题吗,当然有。数据区是临界资源,哦,没有操作系统,那也这么叫吧。你的一切读写操作都要关中断,防止在你操作时中断打断你当前的操作,这么简单吗?恩。
11、 测试吧,好
数据区
uint16_t buffer[128]; //一定是16bit,modbus 03 06 16请求的单位就是WORD
测试代码
int main(void)
{
modbusRTU_open(2,buffer,32); //设置节点地址2,数据长度32,我没有越界!
while(1)
{
modbusRTU_main();
}
}
12、 还有什么
差错控制
不支持的请求功能码要响应
错误的数据地址要响应
错误的数据长度要响应
读写数据区数据出错要响应
怎么实现…… 告诉你的master程序员,你能响应哪些,约定地址范围,就可以了。
一定要响应吗,跟你的master商议,如果一定要响应,在前面检测请求功能码,地址和长度的时候给个响应就行了。
我约定的是超时,也就是不响应,master会重新请求
13、最后
能提高效率吗,那你为什么不选个串口有485功能的的,我对st单片机串口的了解不深,对他深恶痛绝,但是我知道有很多单片机的串口有超时,有地址功能,接收也有FIFO或DMA,那样帧尾就自动判断了,而且地址也自动判断了,接收到的就是你想要的帧
需要源码
251088486@qq.com
可以mail我,我回尽最大努力回复
自我感觉比较胖,可以叫我二师兄或二师弟吧,也可以叫Sattic
欢迎批评指正