FreeModbus移植经验分享

一   为什么要移植Freemodbus

为什么要移植Freemodbus,这个问题需要从两个方面来回答。第一,modbus是一个非常好的应用层协议,它很简洁也相对完善。对于还没有接触过modbus的朋友来说,我非常不建议直接移植freemodbus,应该耐心的从modbus文档入手,并充分把握身边的所有资源,例如PLC的中modbus部分。第二,其实嵌入式系统的通信协议可以自己制定,但是通过实践发现自己定制的协议漏洞百出,尤其是扩展极为困难。我始终认为借鉴他人的经验是很好的途径。借鉴他人成熟的代码,可以减少调试的时间,实现的功能也多了不少。

个人观点,仅供参考。

freemodbus小提示

freemodbus只能使用从机功能。freemodbus更适合嵌入式系统,虽然例子中也有WIN32的例子,如果想要做PC机程序并实现主机功能,推荐使用另一个modbus库——NMODBUS,使用C#开发。同样WINFORM也可以通过自己编写串口代码实现modbus功能,但是这会花费很长的时间,可能是一周也可能是一个月,如果使用现成的代码库,那么开发时间可能只有10分钟。
        自己整理的modbus协议

MODBUS 协议整理.pdf
        代码参考了这个帖子,感谢你的分享。点击这里二  freeemodbus中如何通过串口发送和接收数据

freemodbus通过串口中断的方式接收和发送数据。采用这种做法我想可以节省程序等待的时间,并且也短充分使用CPU的资源。串口中断接收毋庸置疑,在中断服务函数中把数据保存在数组中,以便稍后处理。但是串口发送中断使用哪种形式?串口发送中断至少有两种方式,第一种,数据寄存器空中断,只要数据寄存器为空并且中断屏蔽位置位,那么中断就会发生;第二种,发送完成中断,若数据寄存器的数据发送完成并且中断屏蔽位置位,那么中断也会发送。我非常建议各位使用串口发送完成中断。freemodbus多使用RS485通信中,从机要么接收要么发送,多数情况下从机处于接收状态,要有数据发送时才进入发送状态。进入发送状态时,数据被一个一个字节发送出去,当最后一个字节被发送出去之后,从机再次进入接收状态。如果使用发送寄存器为空中断,还需要使用其他的方法才可以判断最后一个字节的数据是否发送完成。如果使用数据寄存器为空中断,那么将很有可能丢失最后一个字节。(马潮老师的AVR图书中也推荐使用发送完成中断,交流性质的文章,就没有参考文献了。)

二  freemodbus中如何判断帧结束

大家应该清楚,modbus协议中没有明显的开始符和结束符,而是通过帧与帧之间的间隔时间来判断的。如果在指定的时间内,没有接收到新的字符数据,那么就认为收到了新的帧。接下来就可以处理数据了,首当其冲的就是判断帧的合法性。Modbus通过时间来判断帧是否接受完成,自然需要单片机中的定时器配合。

三   整体代码

下面给出一个STM32平台上使用FREEMODBUS最简单的例子,操作保持寄存器,此时操作指令可以为03,06和16;

  • <FONT size=3>#include "stm32f10x.h"
  • #include <stdio.h>
  • #include "mb.h"
  • #include "mbutils.h"
  • //保持寄存器起始地址
  • #define REG_HOLDING_START 0x0000
  • //保持寄存器数量
  • #define REG_HOLDING_NREGS 8
  • //保持寄存器内容
  • uint16_t usRegHoldingBuf[REG_HOLDING_NREGS]
  • = {0x147b,0x3f8e,0x147b,0x400e,0x1eb8,0x4055,0x147b,0x408e};
  • int main(void)
  • {
  • //初始化 RTU模式 从机地址为1 USART1 9600 无校验
  • eMBInit(MB_RTU, 0x01, 0x01, 9600, MB_PAR_NONE);
  • //启动FreeModbus
  • eMBEnable();
  • while (1)
  • {
  • //FreeMODBUS不断查询
  • eMBPoll();
  • }
  • }
  • /**
  • * @brief 保持寄存器处理函数,保持寄存器可读,可读可写
  • * @param pucRegBuffer 读操作时--返回数据指针,写操作时--输入数据指针
  • * usAddress 寄存器起始地址
  • * usNRegs 寄存器长度
  • * eMode 操作方式,读或者写
  • * @retval eStatus 寄存器状态
  • */
  • eMBErrorCode
  • eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
  • eMBRegisterMode eMode )
  • {
  • //错误状态
  • eMBErrorCode eStatus = MB_ENOERR;
  • //偏移量
  • int16_t iRegIndex;
  • //判断寄存器是不是在范围内
  • if( ( (int16_t)usAddress >= REG_HOLDING_START ) \
  • && ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )
  • {
  • //计算偏移量
  • iRegIndex = ( int16_t )( usAddress - REG_HOLDING_START);
  • switch ( eMode )
  • {
  • //读处理函数
  • case MB_REG_READ:
  • while( usNRegs > 0 )
  • {
  • *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] >> 8 );
  • *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] & 0xFF );
  • iRegIndex++;
  • usNRegs--;
  • }
  • break;
  • //写处理函数
  • case MB_REG_WRITE:
  • while( usNRegs > 0 )
  • {
  • usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
  • usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
  • iRegIndex++;
  • usNRegs--;
  • }
  • break;
  • }
  • }
  • else
  • {
  • //返回错误状态
  • eStatus = MB_ENOREG;
  • }
  • return eStatus;
  • }
  • </FONT>

复制代码

先给大家一个整体的印象,先让大家会使用FREEMODBUS,再详细描述细节

//保持寄存器起始地址

#define REG_HOLDING_START     0x0000

//保持寄存器数量

#define REG_HOLDING_NREGS     8

这两个宏定义,决定了保持寄存器的起始地址和总个数。需要强调的是,modbus寄存器的地址有两套规则,一套称为PLC地址,为5位十进制数,例如40001。另一套是协议地址,PLC地址40001意味着该参数类型为保持寄存器,协议地址为0x0000,这里面有对应关系,去掉PLC地址的最高位,然后剩下的减1即可。这会存在一个问题,PLC地址30002和PLC地址40002的协议地址同为0x0001,此时访问时是不是会冲突呢。亲们,当然不会了,30001为输入寄存器,需要使用04指令访问,而40001为保持寄存器,可以使用03、06和16指令访问。所以,用好modbus还是要熟悉协议本生,切不可着急。

//保持寄存器内容

uint16_t usRegHoldingBuf[REG_HOLDING_NREGS]

= {0x147b,0x3f8e,0x147b,0x400e,0x1eb8,0x4055,0x147b,0x408e};

接下来定义了保持寄存器的内容,在这里请大家注意了,保持寄存器为无符号16位数据。在测试的情况下,我随便找了一些数据进行测试。看数据的本质似乎看不出说明规律,但是usRegHoldingBuf却是以16进制保存了浮点数。

  • int main(void)
  • {
  • //初始化 RTU模式 从机地址为1 USART1 9600 无校验
  • eMBInit(MB_RTU, 0x01, 0x01, 9600, MB_PAR_NONE);
  • //启动FreeModbus
  • eMBEnable();
  • while (1)
  • {
  • //FreeMODBUS不断查询
  • eMBPoll();
  • }
  • }

复制代码

接下来就进入主函数部分。有三个FREEMODBUS提供的函数,eMBInit,eMBEnable和eMBPoll。eMBInit为modbus的初始化函数,eMBEnable为modbus的使能函数,而eMBPoll为modbus的查询函数,eMBPoll也是非常单纯的函数,查询是否有数据帧到达,如果有数据到达,便进行相依的处理。再次观察这几个函数,只有eMBInit有很多的参数,这些参数和位于系统底层的硬件有关,这个应该引起移植过程的更多关注。下面几个章节再议。

  • <FONT size=3>eMBErrorCode
  • eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
  • eMBRegisterMode eMode )
  • {
  • //错误状态
  • eMBErrorCode eStatus = MB_ENOERR;
  • //偏移量
  • int16_t iRegIndex;
  • //判断寄存器是不是在范围内
  • if( ( (int16_t)usAddress >= REG_HOLDING_START ) \
  • && ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )
  • {
  • //计算偏移量
  • iRegIndex = ( int16_t )( usAddress - REG_HOLDING_START);
  • switch ( eMode )
  • {
  • //读处理函数
  • case MB_REG_READ:
  • while( usNRegs > 0 )
  • {
  • *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] >> 8 );
  • *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] & 0xFF );
  • iRegIndex++;
  • usNRegs--;
  • }
  • break;
  • //写处理函数
  • case MB_REG_WRITE:
  • while( usNRegs > 0 )
  • {
  • usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
  • usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
  • iRegIndex++;
  • usNRegs--;
  • }
  • break;
  • }
  • }
  • else
  • {
  • //返回错误状态
  • eStatus = MB_ENOREG;
  • }
  • return eStatus;
  • }
  • </FONT>

复制代码

最后,如果收到一个有效的数据帧,那么就可以开始处理了。

第一步,判断寄存器的地址是否在合法的范围内。

if( ( (int16_t)usAddress >= REG_HOLDING_START ) \

&& ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )

第二步,判断需要操作寄存器的偏移地址。

给个例子可以迅速的说明问题,例如访问寄存器的起始地址为0x0002,保持寄存器的起始地址为0x0000,那么这个访问的偏移量为2,程序就从保持寄存器数组的第2个(从0开始)开始操作。

第三步,读写操作分开处理

case MB_REG_READ:

while( usNRegs > 0 )

{

*pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] >> 8 );

*pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] & 0xFF );

iRegIndex++;

usNRegs--;

}

break;

以读操作为例,代码不多说了,请大家注意操作的顺序。保持寄存器以16位形式保存,但是modbus通信时以字节为单位,高位字节数据在前,低位数据字节在后。

四   串口相关部分代码编写

串口部分的代码编写比较常规,主要有三个函数,串口初始化,串口数据发送和串口数据接收。除了以上三个函数之外,还有串口中断服务函数。

  • /**
  • * @brief 串口初始化
  • * @param ucPORT 串口号
  • * ulBaudRate 波特率
  • * ucDataBits 数据位
  • * eParity 校验位
  • * @retval None
  • */
  • BOOL
  • xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
  • {
  • (void)ucPORT; //不修改串口
  • (void)ucDataBits; //不修改数据位长度
  • (void)eParity; //不修改校验格式
  • GPIO_InitTypeDef GPIO_InitStructure;
  • USART_InitTypeDef USART_InitStructure;
  • //使能USART1,GPIOA
  • RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
  • RCC_APB2Periph_USART1, ENABLE);
  • //GPIOA9 USART1_Tx
  • 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);
  • //GPIOA.10 USART1_Rx
  • GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  • GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  • GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮动输入
  • GPIO_Init(GPIOA, &GPIO_InitStructure);
  • USART_InitStructure.USART_BaudRate = ulBaudRate; //只修改波特率
  • USART_InitStructure.USART_WordLength = USART_WordLength_8b;
  • USART_InitStructure.USART_StopBits = USART_StopBits_1;
  • 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);
  • //使能USART1
  • USART_Cmd(USART1, ENABLE);
  • NVIC_InitTypeDef NVIC_InitStructure;
  • NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
  • //设定USART1 中断优先级
  • 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);
  • //最后配置485发送和接收模式
  • RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
  • //GPIOD.8
  • GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
  • GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  • GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  • GPIO_Init(GPIOD, &GPIO_InitStructure);
  • return TRUE;
  • }

复制代码

传入的参数有端口号,波特率,数据位和校验位,可以根据实际的情况修改代码。在这里我并没有修改其他参数,至于传入的波特率是有效的。除了配置串口的相关参数之外,还需要配置串口的中断优先级。最后,由于使用485模式,还需要一个发送接收控制端,该IO配置为推挽输出模式。

  • <FONT size=3>/**
  • * @brief 控制接收和发送状态
  • * @param xRxEnable 接收使能、
  • * xTxEnable 发送使能
  • * @retval None
  • */
  • void
  • vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
  • {
  • if(xRxEnable)
  • {
  • //使能接收和接收中断
  • USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
  • //MAX485操作 低电平为接收模式
  • GPIO_ResetBits(GPIOD,GPIO_Pin_8);
  • }
  • else
  • {
  • USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
  • //MAX485操作 高电平为发送模式
  • GPIO_SetBits(GPIOD,GPIO_Pin_8);
  • }
  • if(xTxEnable)
  • {
  • //使能发送完成中断
  • USART_ITConfig(USART1, USART_IT_TC, ENABLE);
  • }
  • else
  • {
  • //禁止发送完成中断
  • USART_ITConfig(USART1, USART_IT_TC, DISABLE);
  • }
  • }
  • </FONT>

复制代码

由于485使用半双工模式,从机一般处于接收状态,有数据发送时才会进入发送模式。在FreeModbus中有专门的控制接收和发送状态的函数,在这里不但可以打开或关闭接收和发送中断,还可以控制485收发芯片的发送接收端口。代码非常简单,但是还是建议各位使用发送完成中断。

  • <FONT size=3>BOOL
  • xMBPortSerialPutByte( CHAR ucByte )
  • {
  • //发送数据
  • USART_SendData(USART1, ucByte);
  • return TRUE;
  • }
  • BOOL
  • xMBPortSerialGetByte( CHAR * pucByte )
  • {
  • //接收数据
  • *pucByte = USART_ReceiveData(USART1);
  • return TRUE;
  • }
  • xMBPortSerialPutByte和xMBPortSerialGetByte两个函数用于串口发送和接收数据,在这里只要调用STM32的库函数即可。
  • static void prvvUARTTxReadyISR( void )
  • {
  • //mb.c eMBInit函数中
  • //pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM
  • //发送状态机
  • pxMBFrameCBTransmitterEmpty();
  • }
  • static void prvvUARTRxISR( void )
  • {
  • //mb.c eMBInit函数中
  • //pxMBFrameCBByteReceived = xMBRTUReceiveFSM
  • //接收状态机
  • pxMBFrameCBByteReceived();
  • }
  • void USART1_IRQHandler(void)
  • {
  • //发生接收中断
  • if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
  • {
  • prvvUARTRxISR();
  • //清除中断标志位
  • USART_ClearITPendingBit(USART1, USART_IT_RXNE);
  • }
  • //发生完成中断
  • if(USART_GetITStatus(USART1, USART_IT_TC) == SET)
  • {
  • prvvUARTTxReadyISR();
  • //清除中断标志
  • USART_ClearITPendingBit(USART1, USART_IT_TC);
  • }
  • }
  • </FONT>

复制代码

若进入串口中断服务函数,则要调用FreeModbus中响应的函数,串口接收中断服务函数对应prvvUARTRxISR(),其代码如下

  • <FONT size=3>static void prvvUARTRxISR( void )
  • {
  • //mb.c eMBInit函数中
  • //pxMBFrameCBByteReceived = xMBRTUReceiveFSM
  • //接收状态机
  • pxMBFrameCBByteReceived();
  • }
  • </FONT>

复制代码

在prvvUARTRxISR中又调用了pxMBFrameCBByteReceived(),其实pxMBFrameCBTransmitterEmpty()并不是一个函数,而是一个函数指针。其定义如下,请注意函数指针的声明和函数声明的区别。

BOOL( *pxMBFrameCBTransmitterEmpty ) ( void );

在mb.c文件的eMBInit函数完成赋值。一般情况下都会选择RTU模式,那么pxMBFrameCBByteReceived就和xMBRTUReceiveFSM等价了,

pxMBFrameCBByteReceived = xMBRTUReceiveFSM;

同理,若发生串口发送完成中断,该中断服务函数对应prvvUARTTxReadyISR,其代码如下

  • <FONT size=3>static void prvvUARTTxReadyISR( void )
  • {
  • //mb.c eMBInit函数中
  • //pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM
  • //发送状态机
  • pxMBFrameCBTransmitterEmpty();
  • }
  • </FONT>

复制代码

在prvvUARTTxReadyISR中又调用了pxMBFrameCBTransmitterEmpty(),pxMBFrameCBTransmitterEmpty也是函数指针,在eMBInit函数完成赋值,它等价于xMBRTUTransmitFSM。

特别提醒,由于我使用的是串口发送完成中断,想要进入该中断服务函数,需要发送一个字节的数据并启动串口发送中断,代码还需要少许修改。在mbRTU.c的eMBRTUSend中稍作修改,代码如下。

  • <P style="MARGIN: 0cm 0cm 0pt" class=MsoNormal> </P>

复制代码

  • /* First byte before the Modbus-PDU is the slave address. */
  • pucSndBufferCur = ( UCHAR * ) pucFrame - 1;
  • usSndBufferCount = 1;
  • /* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */
  • pucSndBufferCur[MB_SER_PDU_ADDR_OFF] = ucSlaveAddress;
  • usSndBufferCount += usLength;
  • /* Calculate CRC16 checksum for Modbus-Serial-Line-PDU. */
  • usCRC16 = usMBCRC16( ( UCHAR * ) pucSndBufferCur, usSndBufferCount );
  • ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 & 0xFF );
  • ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 >> 8 );
  • /* Activate the transmitter. */
  • //发送状态转换,在中断中不断发送
  • eSndState = STATE_TX_XMIT;
  • //插入代码 启动第一次发送,这样才可以进入发送完成中断
  • xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
  • pucSndBufferCur++;
  • usSndBufferCount--;
  • //使能发送状态,禁止接收状态
  • vMBPortSerialEnable( FALSE, TRUE );
(0)

相关推荐

  • STM32的复用时钟何时开启呢?

    STM32的AFIO时钟真的是在开启引脚复用功能的时候开启吗?其实并不是~ 什么是复用? 我们知道,STM32有很多外设,这些外设的外部引脚都是与GPIO共用的.我们可以通过软件来配置引脚作为GPIO ...

  • STM32系统学习——USART(串口通信)

    串口通信是一种设备间非常常用的串行通行方式,其简单便捷,大部分电子设备都支持. 一.物理层 常用RS-232标准,主要规定了信号的用途.通信接口以及信号的电平标准. "DB9接口" ...

  • 单片机数据通信怎么学?这个工具要用好:串口通信

    刚开始学单片机的你,是不是会因用程序把LED点亮而感到高兴,会因用程序把数码管点亮而感到高兴.这是好事,这也是想继续学习下去的动力. 但是到了与数据相关的实验时,却感觉很难有所进步.有时候,把驱动写好 ...

  • 《嵌入操作系统 - RT-Thread开发笔记》 第二部分 RT-Thread Nano移植与使用 - 第5章 RT-Thread Nano 上移植FinSH

    5 基于 Keil MDK 移植RT-Thread Nano 上移植FinSH 原文地址 本文分为两部分:第一部分是实现 UART 控制台,该部分只需要实现两个函数即可完成 UART 控制台打印功能. ...

  • 附源码-终极串口接收(二)

    来源:公众号[鱼鹰谈单片机] 作者:鱼鹰Osprey ID   :emOsprey 前段时间需要写个串口接收程序,一时没找到源码,就想着自己写过一篇文章<终极串口接收方式,极致效率>,看看 ...

  • STM32与串口屏交互(USART HMI)

    一.前期准备 二.串口屏上位机使用方法以及界面设计 三.STM32软件编程 四.单片机发送数据的字符串指令汇总 五.总结 不管是备战电赛还是准备毕设,一块能与单片机交互的屏幕显得尤为重要,相较于传统的 ...

  • 一文了解串口打印

    之前的文章<STM32 串口详解>介绍了串口驱动,串口在嵌入式领域不仅是一个通讯接口,还是一种调试工具,其好用程度不亚于硬件仿真.有些环境不方便连接Jlink进行硬件仿真,或者并不是必现的 ...

  • 甘以缓之,苦以坚之,口疮外治法经验分享

    外治法治口疮甚于内治.外治之药以甘苦二味为主,所谓甘以缓之,缓解疼痛:苦以坚之.以苦泻之,泻火愈疡.有苦甘化阴,所以制火之说. 患处用药 早自公元420年,医者用黄柏蜜方:"黄柏(削去上皮取 ...

  • 经验分享丨武汉妈妈:高考最后一个月,我是怎么帮助孩子的?

    在我们昨天中午的直播<高考倒计时1个月:分享经验.盘点机会和风险>中,贾小岛老师邀请了往届优秀学生及家长为大家分享最后一个月的安排和部署,相信很多家长昨天都已经看过了. △截自高中生家长圈 ...

  • 【进口经验分享】上海机场进口咖啡粉胶囊报关流程

    胶囊咖啡是将咖啡豆先研磨成咖啡粉,再装进铝质胶囊的,杜绝了普通咖啡豆或者咖啡粉接触空气后变酸,氧化等问题.每次做出来的咖啡都很香,那是因为胶囊很好的保存了咖啡的新鲜度,可以这么说,每一杯都保留了咖啡豆 ...

  • 【进口经验分享】上海机场进口咖啡粉胶囊怎么进口到国内

    胶囊咖啡是将咖啡豆先研磨成咖啡粉,再装进铝质胶囊的,杜绝了普通咖啡豆或者咖啡粉接触空气后变酸,氧化等问题.每次做出来的咖啡都很香,那是因为胶囊很好的保存了咖啡的新鲜度,可以这么说,每一杯都保留了咖啡豆 ...

  • 【进口经验分享】上海港进口咖啡粉胶囊货代操作流程

    胶囊咖啡是将咖啡豆先研磨成咖啡粉,再装进铝质胶囊的,杜绝了普通咖啡豆或者咖啡粉接触空气后变酸,氧化等问题.每次做出来的咖啡都很香,那是因为胶囊很好的保存了咖啡的新鲜度,可以这么说,每一杯都保留了咖啡豆 ...

  • 【进口经验分享】上海洋山港进口咖啡粉胶囊清关手续

    [进口经验分享]上海洋山港进口咖啡粉胶囊清关手续 胶囊咖啡是将咖啡豆先研磨成咖啡粉,再装进铝质胶囊的,杜绝了普通咖啡豆或者咖啡粉接触空气后变酸,氧化等问题.每次做出来的咖啡都很香,那是因为胶囊很好的保 ...

  • 【进口经验分享】上海保税区进口咖啡粉胶囊报关查验方案

    [进口经验分享]上海保税区进口咖啡粉胶囊报关查验方案 胶囊咖啡是将咖啡豆先研磨成咖啡粉,再装进铝质胶囊的,杜绝了普通咖啡豆或者咖啡粉接触空气后变酸,氧化等问题.每次做出来的咖啡都很香,那是因为胶囊很好 ...

  • 【进口经验分享】上海自贸区进口咖啡粉胶囊通关经验

    [进口经验分享]上海自贸区进口咖啡粉胶囊通关经验 胶囊咖啡是将咖啡豆先研磨成咖啡粉,再装进铝质胶囊的,杜绝了普通咖啡豆或者咖啡粉接触空气后变酸,氧化等问题.每次做出来的咖啡都很香,那是因为胶囊很好的保 ...

  • 【进口经验分享】上海外港进口咖啡粉胶囊报关流程

    胶囊咖啡是将咖啡豆先研磨成咖啡粉,再装进铝质胶囊的,杜绝了普通咖啡豆或者咖啡粉接触空气后变酸,氧化等问题.每次做出来的咖啡都很香,那是因为胶囊很好的保存了咖啡的新鲜度,可以这么说,每一杯都保留了咖啡豆 ...