一个高性能串行收发库
嵌入式开发中,你是否遇到以下场景:
1. 调试时,打印数据所花时间过长,超过了被测代码所允许的实时性要求
2.协议解析时,需要高速接收大量数据,发现偶尔会丢失一两个字节数据
如果你遇到上述问题,可以试试ZERO_STRM,它是一个高性能、高速率、使用简单的收发数据工具, 基于 CMSIS-Pack 的,MDK 环境直接装个包就能用
【它能干啥?】
log,传感器数据输出, 指令接收,协议解析,对接shell,printf...
对接printf示例:
MDK环境下使用 printf() 有一种非常通用的方法,参考文章安装对应的 pack 包:【更新】MDK下99%用户都不知道的万能printf方法
装完PACK,完成必要的初始化,然后在代码中对接stdout_putchar(int ch) 完成后 就能愉快的使用printf()了
int stdout_putchar(int ch){ uint8_t chByte = 0; chByte = (uint8_t)(0x000000ff&ch);
if ( zero_strm_write(&g_tZStrmWrite,chByte) ) { return ch; }}
int main(void){ while(1) { printf("hello world \r\n"); perfc_delay_ms(10); }} 
【凭啥用它?】
- 高性能
zero_strm 接收数据流图:

串口接收一个数据的时间,比处理一个数据耗费的时间多得多。可以采取通过空间换时间的策略,来释放CPU时间,提升效率。
具体怎么做呢?
1. 生产者提供一个队列,从Block Fifo 取出去一个Block,将Block作为队列的缓冲区,并将队列初始为满
2. 消费者通过出队接口的读取数据
3. 当队列为空,将队列的初始化缓冲区还给生产者
4. DMA 将Byte Stream 转换为Block,并将Block存入Block Fifo
zero_strm 发送数据流图:

发送数据也可以采用空间换时间的策略,将要发送的数据入队,队列满了后,直接通过DMA (批量)把队列初始化时的缓冲区发送出去。
具体怎么做呢?
1. 消费者提供一个队列,从Block Pool 取出去一个Empty Block,将Empty Block作为队列的缓冲区,并将队列初始为空
2. 生产者通过入队的接口发送数据
3. 当队列为满,将队列初始化的缓冲区交给消费者
4. DMA 处理过程中,从Block Fifo中获取Block,用获取到的Block 初始化DMA,启动DMA 发送。发送完成后,继续获取Block,启动发送,直到Block Fifo 为空
高速率
接收数据采用DMA 半满中断 ,避免了高速率下丢失数据的问题
一般的cotrex-m 芯片是没有链式 DMA 的,在接收大量数据的时候,需要 CPU 的介入切换Block,这就导致 DMA 会暂停,那么暂停是不是意味着就会丢数据呢?
答案是:只需要保证 DMA 暂停的时间小于接收一个字节数据的时间,就能做到不丢失数据
物理层面一个字节数据所流经的路径:
IO-->移位寄存器-->数据寄存器-->MEM
10M 波特率接收一个字节的时间为 1us
CPU的时钟是远高于外设的,以 stm32f103 72M 主频为例,测试暂停 DMA 的时间(配置 DMA 接收所消耗的时间)是 38 个时钟周期 (0.528us)
0.528 < 1 即使是 10M 波特率,stm32f103 也不会出现丢失数据的情况
测试代码:
static void uart_dma_data_get(zero_strm_mem_blk_t *ptThis){ if ( NULL == ptThis ) { return ; }
DMA_Cmd(DMA1_Channel5,DISABLE);
DMA1_Channel5->CMAR = (uint32_t)(this.chMemory); DMA1_Channel5->CNDTR = this.tSizeInByte;
DMA_Cmd(DMA1_Channel5,ENABLE);}
int main(void){ zero_strm_mem_blk_t tFifo; while(1) { __cycleof__("dma exchange") {
uart_dma_data_get(&tFifo); } }}
- 使用简单
发送数据只需要调用zero_strm_write(),接收数据只需要调用zero_strm_read(),用户不用参与复杂的的内存管理
【怎么装?】
先去 GitHub 下个包:
? https://github.com/hoooooz/zero_strm/releases双击
.pack文件,一路 Next,装完收工!打开你的 MDK 工程,点这个图标进 RTE:

找到
Generic,勾选zero_strm,OK!
Generic,加入到工程中了
如果你用的是
Arm Compiler 6,记得把语言改成gnu11~
安装最新版
perf_counter具体介绍说明看链接:【喂到嘴边了的模块】超级嵌入式系统“性能/时间”工具箱安装最新版本
PLOOC具体介绍说明看链接:防御性编程(X)OOPC开发(√)
【怎么用?】
发送数据示例:
zero_strm_write_t g_tZStrmWrite;
__attribute__((aligned(32)))static uint8_t s_chWriteBuffer[1024 * 5];
static zero_strm_write_cfg_t s_tZStrmWriteCfg = { .pchBuffer = s_chWriteBuffer, .hwSize = sizeof(s_chWriteBuffer), .fnDmaSendData = uart_dma_data_send,};/*配置dma 发送函数*/static void uart_dma_data_send(zero_strm_mem_blk_t *ptThis){ if (NULL == ptThis) return;
DMA_Cmd(DMA1_Channel4, DISABLE); DMA1_Channel4->CMAR = (uint32_t)(ptThis->chMemory); DMA1_Channel4->CNDTR = ptThis->tSizeInByte; DMA_Cmd(DMA1_Channel4, ENABLE);}
/*dma 发送完成中断*/void DMA1_Channel4_IRQHandler(void){ if ( RESET != DMA_GetITStatus(DMA1_IT_TC4) ) { DMA_Cmd(DMA1_Channel4,DISABLE); DMA_ClearITPendingBit(DMA1_IT_TC4);
zero_strm_dma_send_data_cpl_event_handler(&g_tZStrmWrite); }}
int main(void){ zero_strm_write_init(&g_tZStrmWrite, &s_tZStrmWriteCfg); while(1) { uint8_t chByte; zero_strm_write(&g_tZStrmWrite, chByte); }
}接收数据示例:
__attribute__((aligned(32)))static uint8_t s_chReadBuffer[1024 * 5];/* 配置dma 读取函数*/static void uart_dma_data_get(zero_strm_mem_blk_t *ptThis){ if ( NULL == ptThis ) { return ; }
DMA_Cmd(DMA1_Channel5,DISABLE);
DMA1_Channel5->CMAR = (uint32_t)(this.chMemory); DMA1_Channel5->CNDTR = this.tSizeInByte;
DMA_Cmd(DMA1_Channel5,ENABLE);}
/* 获取剩余数据函数*/static uint16_t get_dma_cnt(void){ return DMA_GetCurrDataCounter(DMA1_Channel5);}
static zero_strm_read_cfg_t s_tZStrmReadCfg = { .pchBuffer = s_chReadBuffer, .hwSize = sizeof(s_chReadBuffer), .wTimeOutMs = 4000, .fnDmaStartRx = uart_dma_data_get, .fnDmaCntGet = get_dma_cnt,};/* dma 半满中断 */void DMA1_Channel5_IRQHandler(void){ if ( RESET != DMA_GetITStatus(DMA1_IT_HT5) ) { DMA_ClearITPendingBit(DMA1_IT_HT5); zero_strm_uart_dma_get_data_insert_to_dma_irq_event_handler(&g_tZStrmRead); }}/* 串口空闲中断 */void USART1_IRQHandler(void){ if ( RESET != USART_GetITStatus(USART1,USART_IT_IDLE) ) { uint32_t temp = USART1->SR; temp = USART1->DR; (void)temp;
zero_strm_uart_idle_insert_to_uart_irq_event_handler(&g_tZStrmRead); }}/* 定时器周期1ms中断, 配合 wTimeOutMs 4000ms 后自动取回剩余在dma中的数据*/void TIM5_IRQHandler(void){ uint32_t timesr; uint16_t itstatus = 0x0, itenable = 0x0; TIM_TypeDef *TIMx = TIM5;
timesr = TIMx->SR; if (timesr & TIM_IT_Update) { TIMx->SR = (uint16_t)~TIM_IT_Update; zero_strm_uart_wait_time_out_insert_to_hard_timer_irq_event_handler(&g_tZStrmRead) }}
int main(void){ zero_strm_read_init(&g_tZStrmRead,&s_tZStrmReadCfg); while(1) { uint8_t chByte =0; zero_strm_read(&g_tZStrmRead,&chByte) }}注意:单个fifo大小默认是 64Bytes,修改fifo大小 打开zero_strm_cfg.h修改对应的宏就行
【跑得快不快?】
用 STM32F103(72MHz)测试,1M 波特率,1MBytes 数据收发测试,一点没丢!在 3M 波特率的时候,收到的数据会多很多 00 字符,测试下来发现应该是上位机的问题,目前没找到支持高波特率,大量数据发送的上位机 ,找到后继续测试


- 发一个字节只要 85 个时钟周期(1.18us)
- 收一个字节只要 66 个时钟周期(0.916us)
这个是出队入队的时间,实际过程中,当一个队列写满,切换队列会花掉一部分时间。如果出现丢失数据的情况,可以适当加大单个队列的大小,如果还是不行,就需要调整程序策略,匹配生产者和消费者的速率,至于如何调整程序策略这块,可以参考文章:时间和空间的游戏——流/块篇





