本帖最后由 jobszheng 于 2024-7-8 02:13 编辑
概述
数据记录在各行各业应用非常广泛。大到飞机飞行记录的黑闸子,小到我们DIY时的数据交互记录。本次DIY活动利用上市公司武汉芯源的CW32F030C8-startkit开发板设计实现一台串口数据记录仪,实现串口数据,RS485,Modbus协议等通讯环境下的数据记录功能。
架构框图
模块实现
Usart发送与中断接收
在我的项目经验下,我常常使用Usart的中断接收功能,一来,设计起来相对容易;二来,可不依赖硬件,比如某些型号的Usart2外设没有DMA超时中断等。今天给大家带来的实验是CW32F030的Usart1外设与板载CH340进行串口通讯。
使用武汉力源提供的外设驱动库 , 我们非常方便的调用了API就实现了串口初始化、配置、发送与接收功能。
我们依次配置RCC外设,GPIO外设,Usart外设,并打开Usart1_irqn的中断即可。示例代码如下:
static void uart_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_AHBPeriphClk_Enable(RCC_AHB_PERIPH_GPIOA, ENABLE);
RCC_APBPeriphClk_Enable2(RCC_APB2_PERIPH_UART1, ENABLE);
PA08_AFx_UART1TXD();
PA09_AFx_UART1RXD();
GPIO_InitStructure.Pins = GPIO_PIN_8;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
GPIO_Init(CW_GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.Pins = GPIO_PIN_9;
GPIO_InitStructure.Mode = GPIO_MODE_INPUT_PULLUP;
GPIO_Init(CW_GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_Over = USART_Over_16;
USART_InitStructure.USART_Source = USART_Source_PCLK;
USART_InitStructure.USART_UclkFreq = 64000000;
USART_InitStructure.USART_StartBit = USART_StartBit_FE;
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(CW_UART1, &USART_InitStructure);
NVIC_SetPriority(UART1_IRQn, 0);
NVIC_EnableIRQ(UART1_IRQn);
USART_ITConfig(CW_UART1, USART_IT_RC, ENABLE);
}
static void uart_blocksend(const char *ch, uint16_t len)
{
uint16_t i;
for (i = 0; i < len; i++)
{
USART_SendData_8bit(CW_UART1, debug_tx_buf[i]);
while (USART_GetFlagStatus(CW_UART1, USART_FLAG_TXE) == RESET)
;
}
}
void UART1_IRQHandler(void)
{
uint8_t dat;
if (USART_GetITStatus(CW_UART1, USART_IT_RC) != RESET)
{
dat = USART_ReceiveData_8bit(CW_UART1);
USART_ClearITPendingBit(CW_UART1, USART_IT_RC);
if (debug_rx_len < DEBUG_BUF_LEN)
{
debug_rx_buf[debug_rx_len] = dat;
debug_rx_len++;
}
}
}
int main(void)
{
bsp_init();
InitTick(64000000);
while(1)
{
if(time_line_ms == 0)
{
time_line_ms = 500;
PB08_TOG();
PB09_TOG();
log_printf("hello 21ic, hello CW32F030\r\n");
}
}
}
我们通过串口助手工具软件来查看一下输出数据:
对于接收Rx方向,我们本次DIY要实现一个Modbus-RTU从站协议。因此,帧尾的判定我们使用定时器超时的方式处理。定时器我们使用上一个实验中的Systick定时器的1ms计数器来实现。
总结
CW32F030C8的usart外设满足基本需求,在工业控制领域完全胜任。当然,在消费领域肯定就更没有问题了。
I2C外设阻塞式读取CW24C02
EEPROM在项目中的应用非常广泛,因为其非易失性的特性,多用来保存配置参数信息等类似常用,但又有修改需求的数据。
我们本次实验还是使用武汉力源提供的外设驱动库来实现。毕竟是自家的EEPROM,兼容性特别的好,又稳定又可靠!I2C1外设的源代码如下:
int main(void)
{
uint32_t i;
bsp_init();
InitTick(64000000);
log_printf("Hello 21ic, Hello CW32F030\r\n");
while (1)
{
if (time_line_ms == 0)
{
time_line_ms = 500;
PB08_TOG();
PB09_TOG();
}
if ((g_flag & 0x01) == 0x01)
{
g_flag &= ~(0x01);
I2C_MasterWriteEepromData(CW_I2C1, WRITEADDRESS, tx_buf, 8);
log_printf("EEPROM write:\r\n");
for (i = 0; i < 8; i++)
{
log_printf("%02X ", tx_buf[i]);
}
log_printf("\r\n");
}
if ((g_flag & 0x04) == 0x04)
{
g_flag &= ~(0x04);
I2C_MasterReadEepomData(CW_I2C1, WRITEADDRESS, rx_buf, 8);
log_printf("EEPROM read:\r\n");
for (i = 0; i < 8; i++)
{
log_printf("%02X ", rx_buf[i]);
}
log_printf("\r\n");
}
}
}
在本次DIY项目中,我们仅在上电初期读取一下EEPROM中的配置参数,不涉及到频繁读取与写入等需求,因此,我采用阻塞的方式来实现。
总结
得益于武汉力源工作师开发、分享的固件库,我们在实现I2C外设,读取EEPROM时几乎没有遇到困难。I2C外设以状态机的形态来实现,状态转移又清晰又利于编程实现。总之,比某国外32的MCU的I2C不知要好上多少倍?!
SPI Flash写入数据
在低成本的MCU,尤其是Cortex-M0+内核的MCU,其中,对外通讯速率最快的外设便是SPI外设接口。武汉力源CW32F030C8芯片的SPI外设的最大通讯速率为PCLK/4,即64MHz/4 = 16MHz,并且支持4b - 16b模式。这样的灵活参数配置对我们项目开发选择,软件设计带来极大的便利性。
言归正传,我们回到我们本次DIY项目中来,对于大容量的SPI Flash,我们使用它来存储数据,比如日志文件等。SPI Flash的容量大,并且相较于EEPROM的I2C总线,SPI的速率也较高。本次实验实现我们依然借助武汉力源的外设驱动库,源代码如下:
int main(void)
{
uint32_t i;
uint32_t flash_addr = 0;
bsp_init();
InitTick(64000000);
// DeviceID = SPI_FLASH_DeviceID();
// ManufactDeviceID = SPI_FLASH_ManufactDeviceID();
// JedecID = SPI_FLASH_JedecID();
// SPI_FLASH_UniqueID(UniqueID);
// log_printf("\r\nDeviceID = 0x%X\r\nManufactDeviceID = 0x%X\r\nJedecID = 0x%X", DeviceID, ManufactDeviceID, JedecID);
// log_printf("\r\nUniqueID = 0x");
// for (i = 0; i < 8; i++)
// {
// log_printf("%X", UniqueID[i]);
// }
// if (JedecID == sJedecID)
// {
// log_printf("\r\n\nFLASH Detected\r\n");
// }
SPI_FLASH_SectorErase(flash_addr);
for(i = 0; i < 64; i++)
{
tx_buf[i] = i;
}
SPI_FLASH_BufferWrite(tx_buf, flash_addr, 64);
SPI_FLASH_BufferRead(rx_buf, flash_addr, 64);
log_printf("flash read\r\n");
for (i = 0; i < 64; i++)
{
log_printf("%02X ", rx_buf[i]);
}
while (1)
{
if (time_line_ms == 0)
{
time_line_ms = 500;
PB08_TOG();
PB09_TOG();
// log_printf("hello world\r\n");
}
}
}
又是一次轻松实现!示例是通过清除sector,写入64字节的内容,再回读,结果如图所示:
总结
SPI主模式下发送时,由主机发起SCK。注意,在主机回读时,依然要发起一次发送数据,在这次sck驱动下,从机才会把数据回传回到主机,但这次主要发送的数据将会被从机丢掉而已。
CRC-16硬件实现
我们都知道Modbus-RTU协议中每帧数据都有crc-16校验,以保证传输命令的正确性,因为多应用于工业控制领域,一条错误的命令,机器的执行动作可能将导致严重的后果。在武汉力源芯片之前,我会在我的软件代码里面非常熟练的实现CRC-16的校验算法,为了加快计算,我还会把与或值以查询表的形式实现,以求空间换时间,但现在使用武汉力源的CW32F030后,我只需要写入CRC模块寄存器值即可,其会自动输出校验结果。方便快捷!其代码也只有这一行,而实现也才8行而已!
uint16_t CRC16_Calc_8bit(uint8_t CrcMode, uint8_t *pByteBuf, uint16_t ByteCnt)
{
CW_CRC->CR = CrcMode;
while (ByteCnt)
{
CW_CRC->DR8 = *pByteBuf;
pByteBuf++;
ByteCnt--;
}
return (CW_CRC->RESULT16);
}
总结
这个CRC校验模块不仅包含了我最常用的CRC-16 Modbus算法,还有其它7种算法。当然,模块也有CRC32算法。
串口记录仪原型开发作品
前面我们完成了本次DIY作品的各个模块的实现,现在我们依据我们的串口记录仪的原型开发方案来实现我们工程。
引脚分配
本次DIY活动使用的武汉芯源CW32F030-startkit开发板,引脚分配非常灵活,再加上芯片本身的IO复用功能非常强大。所以,我们本次的引脚分布如下:
PA06,UART2_TXD
PA07.UART2_RXD
PA08.UART1_TXD
PA09,UART1_RXD
PB06,I2C1_SCL
PB07,I2C1_SDA
PB12,SPI1_CS
PB13,SPI1_SCK
PB14,SPI1_MISO
PB15,SPI1_MOSI
Modbus-RTU协议
Modbus协议在工业控制领域应用非常广泛,其协议简单,严谨,包含指令,数据及CRC校验等。在工业控制领域中,Modbus协议部署的设备数量最多,可以说是事实的工业标准。
本次我们不在这里讲解Modbus的全部实现,仅讲解Modbus协议的CRC实现。对于Modbus-RTU协议,每帧数据以CRC-16结尾来验证此帧数据是否在传输中出现错误。错误则忽略此帧,等待主站对此帧的重传。以前,我在实现modbus-rtu-RTU时,只能软件来实现,如下图所用算法,但现在我可以使用硬件模块来实现,我甚至将其实现在中断中,毕竟只有一个PCLK的时延——系统肯定可以接受,这个功能极大改善了我的软件代码的质量。
static const UCHAR aucCRCHi[] = {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40
};
static const UCHAR aucCRCLo[] = {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7,
0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E,
0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9,
0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC,
0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32,
0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D,
0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38,
0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF,
0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1,
0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4,
0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB,
0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA,
0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,
0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97,
0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E,
0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89,
0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83,
0x41, 0x81, 0x80, 0x40
};
USHORT
usMBCRC16( UCHAR * pucFrame, USHORT usLen )
{
UCHAR ucCRCHi = 0xFF;
UCHAR ucCRCLo = 0xFF;
int iIndex;
while( usLen-- )
{
iIndex = ucCRCLo ^ *( pucFrame++ );
ucCRCLo = ( UCHAR )( ucCRCHi ^ aucCRCHi[iIndex] );
ucCRCHi = aucCRCLo[iIndex];
}
return ( USHORT )( ucCRCHi << 8 | ucCRCLo );
}
EEPROM中存储匹配参数
在上电初期,我们首先从EEPROM中读取配置参数与过滤规则,比如我们仅保留Modbus主站发出来的ID为0x01的帧数据。做为示例,我们将过滤规则定义如下数据结构:
typedef struct mb_filter_s
{
uint8_t id_mask;
uint8_t func_id_mask;
uint8_t len_mask;
uint8_t err_mask;
}mb_filter_t;
SPI Flash的日志数据存储
在本次DIY采用的是直接写入模式。限于时间关系,未实现文件系统,但8KB的SRAM及64KB的Flash容量实现文件系统足矣。
不过,虽然没有实现文件系统,但我们仍然要实现一个日志的info头数据
typedef struct log_info_s
{
uint8_t flag; // fixed to 0xA5;
uint8_t len;
uint8_t next;
uint8_t mode;
}log_info_t;
视频展示
|