当前位置:首页 > 公众号精选 > 裸机思维

嵌入式开发中,你是否遇到以下场景:

1. 调试时,打印数据所花时间过长,超过了被测代码所允许的实时性要求

2.协议解析时,需要高速接收大量数据,发现偶尔会丢失一两个字节数据

如果你遇到上述问题,可以试试ZERO_STRM,它是一个高性能、高速率、使用简单的收发数据工具, 基于 CMSIS-Pack 的,MDK 环境直接装个包就能用


【它能干啥?】

log,传感器数据输出, 指令接收,协议解析,对接shell,printf...

对接printf示例:

MDK环境下使用 printf() 有一种非常通用的方法,参考文章安装对应的 pack 包:【更新】MDK下99%用户都不知道的万能printf方法

装完PACK,完成必要的初始化,然后在代码中对接stdout_putchar(int ch) 完成后 就能愉快的使用printf()了


#include 
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(),用户不用参与复杂的的内存管理


【怎么装?】

  1. 先去 GitHub 下个包:
    ? https://github.com/hoooooz/zero_strm/releases

  2. 双击 .pack 文件,一路 Next,装完收工!

  3. 打开你的 MDK 工程,点这个图标进 RTE:

  4. 找到 Generic,勾选 zero_strm,OK!

  5. Generic,加入到工程中了

  6. 如果你用的是 Arm Compiler 6,记得把语言改成 gnu11 ~

  7. 安装最新版 perf_counter 具体介绍说明看链接:【喂到嘴边了的模块】超级嵌入式系统“性能/时间”工具箱

  8. 安装最新版本PLOOC 具体介绍说明看链接:防御性编程(X)OOPC开发(√)


【怎么用?】

发送数据示例:


#include "zero_strm.h"
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修改对应的宏就行


#define READ_BUFF_CNT     (64*2)#define WRITE_BUFF_CNT    (64)

【跑得快不快?】

用 STM32F103(72MHz)测试,1M 波特率,1MBytes 数据收发测试,一点没丢!在 3M 波特率的时候,收到的数据会多很多 00 字符,测试下来发现应该是上位机的问题,目前没找到支持高波特率,大量数据发送的上位机 ,找到后继续测试

  • 发一个字节只要 85 个时钟周期(1.18us)
  • 收一个字节只要 66 个时钟周期(0.916us)

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

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭