单片机

本文主要介绍在嵌入式开发中用来输出log的方法。

最常用的是通过串口输出uart log,这种方法实现简单,大部分嵌入式芯片都有串口功能。但是这样简单的功能有时候却不是那么好用,比如:

  • 一款新拿到的芯片,没有串口驱动时如何打印log

  • 某些应用下对时序要求比较高,串口输出log占用时间太长怎么办?比如USB枚举。

  •  某些bug正常运行时会出现,当打开串口log时又不再复现怎么办

  • 一些封装中没有串口,或者串口已经被用作其他用途,要如何输出log 

下文来讨论这些问题。

1、输出log信息到SRAM

准确来说这里并不是输出log,而是以一种方式不使用串口就可以看到log。在芯片开发阶段都可以连接仿真器调试,可以使用打断点的方法调试,但是有些操作如果不能被打断就没法使用断点调试了。

这时候可以考虑将log打印到SRAM中,整个操作结束后再通过仿真器查看SRAM中的log buffer,这样就实现了间接的log输出。

本文使用的测试平台是STM32F407 discovery,基于usb host实验代码,对于其他嵌入式平台原理也是通用的。首先定义一个结构体用于打印log,如下:

“不用串口,如何打印STM32单片机log"

定义一段SRAM空间作为log buffer:

static u8 log_buffer[LOG_MAX_LEN];

log buffer是环形缓冲区,在小的buffer就可以无限打印log,缺点也很明显,如果log没有及时输出就会被新的覆盖。Buffer大小根据SRAM大小分配,这里使用1kB。为了方便输出参数,使用printf函数来格式化输出,需要做如下配置(Keil):

“不用串口,如何打印STM32单片机log"

并包含头文件#include <stdio.h>, 在代码中实现函数fputc():

“不用串口,如何打印STM32单片机log"

写入数据到SRAM:

“不用串口,如何打印STM32单片机log"

为了方便控制log打印格式,在头文件中再添加自定义的打印函数。

“不用串口,如何打印STM32单片机log"

在需要打印log的地方直接调用DEBUG()即可,最终效果如下,从Memory窗口可以看到打印的log:

“不用串口,如何打印STM32单片机log"

2、通过SWO输出log

通过打印log到SRAM的方式可以看到log,但是数据量多的时候可能来不及查看就被覆盖了。为了解决这个问题,可以使用St-link的SWO输出log,这样就不用担心log被覆盖。查看原理图f407 discovery的SWO已经连接了,否则需要自己飞线连接:

“不用串口,如何打印STM32单片机log"

在log结构体中添加SWO的操作函数集:

typedef struct
{
    u8 (*init)(void* arg);
    u8 (*print)(u8 ch);
    u8 (*print_dma)(u8* buffer, u32 len);
}log_func;

typedef struct 
{
    volatile u8     type;
    u8*             buffer;
    volatile u32    write_idx;
    volatile u32    read_idx;
    //SWO
    log_func*       swo_log_func;
}log_dev;

SWO只需要print操作函数,实现如下:

u8 swo_print_ch(u8 ch)
{
    ITM_SendChar(ch);
    return 0;
}

使用SWO输出log同样先输出到log buffer,然后在系统空闲时再输出,当然也可以直接输出。log延迟输出会影响log的实时性,而直接输出会影响到对时间敏感的代码运行,所以如何取舍取决于需要输出log的情形。

在while循环中调用output_ch()函数,就可以实现在系统空闲时输出log。

/*output log buffer to I/O*/
void output_ch(void)
{   
    u8 ch;
    volatile u32 tmp_write,tmp_read;
    tmp_write = log_dev_ptr->write_idx;
    tmp_read = log_dev_ptr->read_idx;

    if(tmp_write != tmp_read)
    {
        ch = log_dev_ptr->buffer[tmp_read++];
        //swo
        if(log_dev_ptr->swo_log_func)
            log_dev_ptr->swo_log_func->print(ch);
        if(tmp_read >= LOG_MAX_LEN)
        {
            log_dev_ptr->read_idx = 0;
        }
        else
        {
            log_dev_ptr->read_idx = tmp_read;
        }
    }
}

2.1 通过IDE输出

使用IDE中SWO输出功能需要做如下配置(Keil):

“不用串口,如何打印STM32单片机log"

在窗口可以看到输出的log:

“不用串口,如何打印STM32单片机log"

2.2 通过STM32 ST-LINK Utility输出

使用STM32 ST-LINK Utility不需要做特别的设置,直接打开ST-LINK菜单下的Printf via SWO viewer,然后按start:

“不用串口,如何打印STM32单片机log"

3、通过串口输出log

以上都是在串口log暂时无法使用,或者只是临时用一下的方法,而适合长期使用的还是需要通过串口输出log,毕竟大部分时候没法连接仿真器。添加串口输出log只需要添加串口的操作函数集即可:

typedef struct 
{
    volatile u8     type;
    u8*             buffer;
    volatile u32    write_idx;
    volatile u32    read_idx;
    volatile u32    dma_read_idx;
    //uart
    log_func*       uart_log_func;
    //SWO
    log_func*       swo_log_func;
}log_dev;

实现串口驱动函数:

“不用串口,如何打印STM32单片机log?"

添加串口输出log与通过SWO过程类似,不再多叙述。而下面要讨论的问题是,串口的速率较低,输出数据需要较长时间,严重影响系统运行。

虽然可以通过先打印到SRAM再延时输出的办法来减轻影响,但是如果系统中断频繁,或者需要做耗时运算,则可能会丢失log。要解决这个问题,就是要解决CPU与输出数据到串口同时进行的问题,嵌入式工程师立马可以想到DMA正是好的解决途径。

使用DMA搬运log数据到串口输出,同时又不影响CPU运行,这样就可以解决输出串口log耗时影响系统的问题。串口及DMA初始化函数如下:

u8 uart_log_init(void* arg)
{
    DMA_InitTypeDef DMA_InitStructure;
    u32* bound = (u32*)arg;
    //GPIO端口设置
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//使能USART2时钟
    //串口2对应引脚复用映射
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2);
    //USART2端口配置
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;   //速度50MHz
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    //USART2初始化设置
    USART_InitStructure.USART_BaudRate = *bound;//波特率设置
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
    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_Tx; //收发模式
    USART_Init(USART2, &USART_InitStructure); //初始化串口1
    #ifdef LOG_UART_DMA_EN  
    USART_DMACmd(USART2,USART_DMAReq_Tx,ENABLE);
    #endif  
    USART_Cmd(USART2, ENABLE);  //使能串口1 
    USART_ClearFlag(USART2, USART_FLAG_TC);
    while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
    #ifdef LOG_UART_DMA_EN
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
    //Config DMA channel, uart2 TX usb DMA1 Stream6 Channel
    DMA_DeInit(DMA1_Stream6);
    DMA_InitStructure.DMA_Channel = DMA_Channel_4;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART2->DR);
    DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; 
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
    DMA_Init(DMA1_Stream6, &DMA_InitStructure);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
    #endif
    return 0;
}

DMA输出到串口的函数如下:

“不用串口,如何打印STM32单片机log"

这里为了方便直接使用了查询DMA状态寄存器,有需要可以修改为DMA中断方式,查Datasheet可以找到串口2使用DMA1 channel4的stream6:

“不用串口,如何打印STM32单片机log"

最后在PC端串口助手可以看到log输出:

“不用串口,如何打印STM32单片机log"

使用DMA搬运log buffer中数据到串口,同时CPU可以处理其他事情,这种方式对系统影响最小,并且输出log及时,是实际使用中用的最多的方式。并且不仅可以用串口,其他可以用DMA操作的接口(如SPI、USB)都可以使用这种方法来打印log。

4、使用IO口模拟串口输出log

最后要讨论的是在一些封装中没有串口,或者串口已经被用作其他用途时如何输出log,这时可以找一个空闲的普通IO,模拟UART协议输出log到上位机的串口工具。常用的UART协议如下:

“不用串口,如何打印STM32单片机log"

只要在确定的时间在IO上输出高低电平就可以模拟出波形,这个确定的时间就是串口波特率。为了得到精确延时,这里使用TIM4定时器产生1us的延时。注意:定时器不能重复用,在测试工程中TIM2、3都被用了,如果重复用就错乱了。初始化函数如下:

u8 simu_log_init(void* arg)
{
    TIM_TimeBaseInitTypeDef TIM_InitStructure;  
    u32* bound = (u32*)arg;
    //GPIO端口设置
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;   //速度50MHz
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    GPIO_SetBits(GPIOA, GPIO_Pin_2);
    //Config TIM
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE); //使能TIM4时钟
    TIM_DeInit(TIM4);
    TIM_InitStructure.TIM_Prescaler = 1;        //2分频
    TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_InitStructure.TIM_Period = 41;          //1us timer
    TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM4, &TIM_InitStructure);
    TIM_ClearFlag(TIM4, TIM_FLAG_Update);
    baud_delay = 1000000/(*bound);          //根据波特率计算每个bit延时
    return 0;
}

使用定时器的delay函数为:

“不用串口,如何打印STM32单片机log"

最后是模拟输出函数,注意:输出前必须要关闭中断,一个byte输出完再打开,否则会出现乱码:

u8 simu_print_ch(u8 ch)
{
    volatile u8 i=8;
    __asm("cpsid i");
    //start bit
    GPIO_ResetBits(GPIOA, GPIO_Pin_2);
    simu_delay(baud_delay);
    while(i--)
    {
        if(ch & 0x01)
        GPIO_SetBits(GPIOA, GPIO_Pin_2);
        else
        GPIO_ResetBits(GPIOA, GPIO_Pin_2);
        ch >>= 1;
        simu_delay(baud_delay);
    }
    //stop bit
    GPIO_SetBits(GPIOA, GPIO_Pin_2);
    simu_delay(baud_delay);
    simu_delay(baud_delay);
    __asm("cpsie i");
    return 0;
}

使用IO模拟可以达到与真实串口类似的效果,并且只需要一个普通IO,在小封装芯片上比较使用。

来源:STM32嵌入式开发
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 575

NorthFrame是基于非UML极简理念的状态机框架

配合NF_FsmDesigner图形化开发工具,可无负担替代传统switch-case状态机开发。

1、NorthFrame的组件

  • NF_FSM :

    极简非UML状态机框架

  • NF_FsmDesigner :

    基于C# Winform开发的状态机图形化开发工具,可直接生成C代码

  • NF_Signal :

    用于代替全局变量的动态信号机制

NF_Signal :代替全局变量,使用方便:

NF_Signal_Set("flag_connect", 1);
NF_Signal_Set("blink_cnt", 3);
NF_SignalValue flag_connect = NF_Signal_Get("flag_connect");

2、NorthFrame图形化状态机开发

以下例程在VS2012环境中运行一个判断QE组合键的状态机:

Step1 : 使用NF_FsmDesigner工具设计绘制状态转换图,并保存为XML文件

“基于单片机的极简图形化状态机框架NorthFrame"

Step2 : 点击生成代码,生成如下C语言代码

#include <n_frame.h>
#include <fsm_qande.h>

/* 转换执行的外部函数声明 */
extern void IDLE_TO_Q(void);
extern void Q_TO_QE(void);
extern void QE_TO_IDLE(void);
extern void QE_TO_Q(void);
extern void Q_TO_IDLE(void);

/* 状态处理函数声明 */
void FSM_QandE_IDLE(NF_FSM* me, NF_Event event);
void FSM_QandE_Q_DOWN(NF_FSM* me, NF_Event event);
void FSM_QandE_QE_DOWN(NF_FSM* me, NF_Event event);

/* 状态机对象 */
NF_FSM FSM_QandE = {
    FSM_QandE_IDLE
};

/* IDLE状态处理函数 */
void FSM_QandE_IDLE(NF_FSM* me, NF_Event event)
{

    if (NF_FSM_NameIs(event.Name, "Q_DOWN"))
    {
        IDLE_TO_Q();
        NF_FSM_TRAN(FSM_QandE_Q_DOWN);
        return ;
    }

    if (NF_FSM_NameIs(event.Name, "test"))
    {
        NF_FSM_TRAN(FSM_QandE_IDLE);
        return ;
    }
}

/* Q_DOWN状态处理函数 */
void FSM_QandE_Q_DOWN(NF_FSM* me, NF_Event event)
{

    if (NF_FSM_NameIs(event.Name, "E_DOWN"))
    {
        Q_TO_QE();
        NF_FSM_TRAN(FSM_QandE_QE_DOWN);
        return ;
    }

    if (NF_FSM_NameIs(event.Name, "Q_UP"))
    {
        Q_TO_IDLE();
        NF_FSM_TRAN(FSM_QandE_IDLE);
        return ;
    }
}

/* QE_DOWN状态处理函数 */
void FSM_QandE_QE_DOWN(NF_FSM* me, NF_Event event)
{

    if (NF_FSM_NameIs(event.Name, "Q_UP"))
    {
        QE_TO_IDLE();
        NF_FSM_TRAN(FSM_QandE_IDLE);
        return ;
    }

    if (NF_FSM_NameIs(event.Name, "E_UP"))
    {
        QE_TO_Q();
        NF_FSM_TRAN(FSM_QandE_Q_DOWN);
        return ;
    }
}

Step3 : 在main.c文件中实现按键处理,并发送事件给状态机

备注 : 后续版本会加入发布-订阅机制,目前仅支持直接派发

#include "n_frame.h"

#include "windows.h"
#include "stdio.h"

#include "fsm_qande.h"

#define KEY_VALUE(_key) ((GetKeyState(_key) >= 0) ? NF_Bool_False : NF_Bool_True )

/* 信号产生者 */
void Test_Key_Process(void)
{
 static NF_Bool last_q_val = NF_Bool_False;
 static NF_Bool last_e_val = NF_Bool_False;

 NF_Bool then_q_val;
 NF_Bool then_e_val;

 then_q_val = KEY_VALUE('Q');
 then_e_val = KEY_VALUE('E');

 /* Q键事件处理 */
 if ((last_q_val == NF_Bool_False) && (KEY_VALUE('Q') == NF_Bool_True))
 {
  NF_FSM_Dispatch(&FSM_QandE, NF_FSM_Event("Q_DOWN"));
 } 
 else if ((last_q_val == NF_Bool_True) && (KEY_VALUE('Q') == NF_Bool_False))
 {
  NF_FSM_Dispatch(&FSM_QandE, NF_FSM_Event("Q_UP"));
 }

 /* E键事件处理 */
 if ((last_e_val == NF_Bool_False) && (KEY_VALUE('E') == NF_Bool_True))
 {
  NF_FSM_Dispatch(&FSM_QandE, NF_FSM_Event("E_DOWN"));
 } 
 else if ((last_e_val == NF_Bool_True) && (KEY_VALUE('E') == NF_Bool_False))
 {
  NF_FSM_Dispatch(&FSM_QandE, NF_FSM_Event("E_UP"));
 }

 last_q_val = then_q_val;
 last_e_val = then_e_val;
}

void IDLE_TO_Q(void)
{
 printf("state translate : IDLE -> Q_DOWN\n");
}

void Q_TO_QE(void)
{
 printf("state translate : Q_DOWN -> QE_DOWN\n");
}

void QE_TO_IDLE(void)
{
 printf("state translate : QE_DOWN -> IDLE\n");
}

void QE_TO_Q(void)
{
 printf("state translate : QE_DOWN -> Q_DOWN\n");
}

void Q_TO_IDLE(void)
{
 printf("state translate : Q_DOWN -> IDLE\n");
}

int main(void)
{
 for (;;)
 {
  Test_Key_Process();
 }
}

克隆链接:

git clone https://gitee.com/PISCES_X/NorthFrame.git

来源:嵌入式云IOT技术圈
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 77

对于从事单片机应用系统(软硬件)设计的工程技术人员来说,掌握一定的EMC测试技术是十分必要的。

关于EMC

EMC:Electromagnetic Compatibility,即电磁兼容性。指设备或系统在其电磁环境中符合要求运行并不对其环境中的任何设备产生无法忍受的电磁骚扰的能力。

它包括电磁干扰(EMI)和电磁敏感性(EMS)两部分。由于电器产品在使用时对其它电器有电磁干扰,或受到其它电器的电磁干扰,它不仅关系到产品工作的可靠性和安全性,还可能影响其它电器的正常工作,甚至导致安全危险。

EMC测试两大内容

1、对其向外界发送的电磁骚扰强度进行测试,以便确认是否符合有关标准规定的限制值要求;

2、对其在规定电磁骚扰强度的电磁环境条件下进行敏感度测试,以便确认是否符合有关标准规定的抗扰度要求。

单片机系统EMC测试

1、测试环境

为了保证测试结果的准确和可靠性,电磁兼容性测量对测试环境有较高的要求,测量场地有室外开阔场地、屏蔽室或电波暗室等。

2、测试设备

电磁兼容测量设备分为两类:一类是电磁干扰测量设备,设备接上适当的传感器,就可以进行电磁干扰的测量;另一类是在电磁敏感度测量,设备模拟不同干扰源,通过适当的耦合/去耦网络、传感器或天线,施加于各类被测设备,用作敏感度或干扰度测量。

3、测量方法

电磁兼容性测试依据标准的不同,有许多种测量方法,但归纳起来可分为4类;传导发射测试、辐射发射测试、传导敏感度(抗扰度)测试和辐射敏感度(抗扰度)测试。

4、测试诊断步骤

图1给出了一个设备或系统的电磁干扰发射与故障分析步骤。按照这个步骤进行,可以提高测试诊断的效率。

“单片机系统EMC测试和故障排除"

5、测试准备

①试验场地条件:EMC测试实验室为电波半暗室和屏蔽室。前者用于辐射发射和辐射敏感测试,后者用于传导发射和传导敏感度测试。

②环境电平要求:传导和辐射的电磁环境电平最好远低于标准规定的极限值,一般使环境电平至少低于极限值6dB。

③试验桌。

④测量设备和被测设备的隔离。

⑤敏感性判别准则:一般由被测方提供,并实话监视和判别,以测量和观察的方式确定性能降低的程度。

⑥被测设备的放置:为保证实验的重复性,对被测设备的放置方式通常有具体的规定。

6、测试种类

传导发射测试、辐射发送测试、传导抗扰度测试、辐射抗扰度测试。

7、常用测量仪

电磁干扰(EMI)和电磁敏感度(EMS)测试,需要用到许多电子仪器,如频谱分析仪、电磁场干扰测量仪、信号源、功能放大器、示波器等。由于EMC测试频率很宽(20Hz~40GHz)、幅度很大(μV级至kW级)、模式很多(FM、AM等)、姿态很多(平放、斜放等),因此正确地使用电子仪器非常重要。

测量电磁干扰的合适仪器是频谱分析仪。频谱分析仪是一种将电压幅度随频率变化的规律显示出来的仪器,它显示的波形称为频谱。频谱分析仪克服了示波器在测量电磁干扰中的缺点,能够精确测量各个频率上的干扰强度,用频谱分析仪可以直接显示出信号的各个频谱分量。

电磁兼容故障排除技术

1、传导型问题的解决

①通过串联一个高阻抗来减少EMI电流。

②通过并联一个低阻抗将EMI电流短路到地或引到其它回路导体。

③通过电流隔离装置切断EMI电流。

④通过其自身作用来抑制EMI电流。

2、电磁兼容的容性解决方案

一种常见的现象是不把滤波电容的一侧看成直接与一个分离的阻抗相连,而看成与传输线相连。典型的情况是,当一条输入输出线的长度达到或超过1/4波长时,该传输线变“长”。

实际可以用下式近似表示这种变化:l≥55/f

式中:l单元为m,f单位为MHz。这个公式考虑了平均传播速度,它是自由空间理论的0.75倍。

a. 电介质材料及容差

电磁干扰滤波使用的大部分电容是无极性电容。

b. 差模(线到线)滤波电容性电容。

c. 共模(线到地/机壳)滤波电容

共模(CM)去耦通常使用小电容(10~100nF)。小电容可以将不期望的高频电流在其进入敏感电路之前或在其离噪声电路较远时就将其短路到机壳上去。为了得到良好的高频衰减电路,减小或消除寄生电感是关键之所在。因此有必要使用超短导线,尤其希望使用无引线元器件。

3、感性、串联损耗电磁兼容解决方案

就电容而言,Zs和Z1如果不是纯电阻的话,在计算频率时,要使用它们的实际值。电容器串联在电源或信号电路时,必须满足:

①流过的工作电流不应该引起电感过热或过大的有过之而无不及降;

②流过的电流不能引起电感磁饱和,尤其是对高导磁材料是毫无疑问的。

解决方案有以下几种:

  • 磁芯材料;

  • 铁氧体和加载铁氧体的电缆;

  • 电感、差模和共模;

  • 接地扼流圈;

  • 组合式电感电容元件。

4、辐射型问题的解决

在很多情况下,辐射电磁干扰问题可能在传导阶段产生并被排除,还有些解决方案是可以抑制干扰装置在辐射传输通道上,就像场屏蔽那样工作。根据屏蔽理论,这种屏蔽的效果主要取决于电磁干扰源的频率、与屏蔽装置之间的距离以及电磁干扰场的特性——电场、磁场或者平面波。

结语

在实际EMC测试应用中,除了通过标准资格实验室的鉴定测试以外,还有两种可行的方法也是被业界所认可的:TCF(Technical ConstrucTIon File)和Self CeriTIfication(自检证明)。

抗干扰能力测试是十分实用的测试项目。实现电磁兼容的最好办法是,将所有的数字及模拟电路均视为对高频信号响应的电路,用高频设计方法来处理电费屏蔽、PCB布线和共模滤波。采用整块地平面和电源面也很重要,对模拟电路也该如此,这样做有利于限制高频共模环环。大多数瞬态干扰均属高频,并产生很强的辐射能量。

来源:ST中文论坛
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 37

在编写单片机程序的时候,由于中断服务程序写的不好,导致单片机程序总是跑飞,最后费了好长时间,花了很大功夫才找到问题原因,由此总结了单片机程序跑飞的三种现象、原因及解决方法。

1、数组越界/溢出

现象:

单片机程序在函数中运行时,总是在运行到函数末尾,要跳出函数时,程序跑飞。

原因:

数组越界(数组溢出),函数中定义的数组元素的个数小于程序中实际使用的数组元素的个数,例如在函数中定义了一个数组ucDataBuff[10],这个数组只有10个元素,但是在函数中却有这样的语句ucDataBuff[10]=0x1a,这个语句是给数组的第11个元素赋值,:由于定义的数组只有10个元素,从而导致赋值语句中不知道把0x1a放到什么地方,从而导致程序跑飞。

解决方法:

如果在调试程序时,发现程序总是在函数执行完毕时跑飞,多数情况是发生了数组越界(数组溢出)的错误,仔细检查函数中调用的数组是否存在越界(溢出)的情况。

2、中断服务程序缺失

现象:

程序运行过程中总是跑飞。

原因:

程序中打开了某个中断,但是却没有相应的中断服务程序,从而导致在中断发生后,找不到中断服务程序入口,从而导致程序跑飞。

解决方法:

检查程序中是否存在打开了某个中断,但是没有相对应的中断服务程序。

3、看门狗复位

现象:

在执行一段较为耗费时间的程序时,程序跑飞,并且总是跳到复位位置处。

原因:

程序中使用了看门狗,但是没有及时“喂狗”,从而导致看门狗复位,使程序直接跳到复位位置。

解决方法:

根据程序运行时间,尤其是一定要计算清楚最耗时的那段程序的运行时间,然后准确设置看门狗的复位时长,定时“喂狗”,尤其是如果有死循环的情况,一定要在死循环中记得“喂狗”。

来源:网络
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 634

三极管在数字电路里的开关特性,最常见的应用有 2 个:一个是控制应用,一个是驱动应用。所谓的控制就是,我们可以通过单片机控制三极管的基极来间接控制后边的小灯的亮灭,用法大家基本熟悉了。

还有一个控制就是进行不同电压之间的转换控制,比如我们的单片机是 5V 系统,它现在要跟一个 12V 的系统对接,如果 IO 直接接 12V电压就会烧坏单片机,所以我们加一个三极管,三极管的工作电压高于单片机的 IO 口电压,用 5V 的 IO 口来控制 12V 的电路,如图 1 所示。

“图1
图1 三极管实现电压转换

图1中,当 IO 口输出高电平 5V 时,三极管导通,OUT 输出低电平 0V,当 IO 口输出低电平时,三极管截止,OUT 则由于上拉电阻 R2 的作用而输出 12V 的高电平,这样就实现了低电压控制高电压的工作原理。

所谓的驱动,主要是指电流输出能力。我们再来看如图2中两个电路之间的对比。

“图2
图2 LED 小灯控制方式对比

图2中上边的 LED 灯,和我们第二课讲过的 LED 灯是一样的,当 IO 口是高电平时,小灯熄灭,当 IO 口是低电平时,小灯点亮。那么下边的电路呢,按照这种推理,IO 口是高电平的时候,应该有电流流过并且点亮小灯,但实际上却并非这么简单。

单片机主要是个控制器件,具备四两拨千斤的特点。就如同杠杆必须有一个支点一样,想要撑起整个地球必须有力量承受的支点。

单片机的 IO 口可以输出一个高电平,但是他的输出电流却很有限,普通 IO 口输出高电平的时候,大概只有几十到几百 uA 的电流,达不到1mA,也就点不亮这个 LED 小灯或者是亮度很低,这个时候如果我们想用高电平点亮 LED,就可以用上三极管来处理了,我们板上的这种三极管型号,可以通过 500mA 的电流,有的三极管通过的电流还更大一些,如图 3所示。

“图3
图3 三极管驱动 LED 小灯

图3中,当 IO 口是高电平,三极管导通,因为三极管的电流放大作用,c 极电流就可以达到 mA 以上了,就可以成功点亮 LED 小灯。

虽然我们用了 IO 口的低电平可以直接点亮 LED,但是单片机的 IO 口作为低电平,输入电流就可以很大吗?

这个我想大家都能猜出来,当然不可以。单片机的 IO 口电流承受能力,不同型号不完全一样,就 STC89C52 来说,官方手册的 81 页有对电气特性的介绍,整个单片机的工作电流,不要超过 50mA,单个 IO 口总电流不要超过 6mA。

即使一些增强型 51 的IO 口承受电流大一点,可以到 25mA,但是还要受到总电流 50mA 的限制。那我们来看电路图的 8 个 LED 小灯这部分电路,如图4所示。

“图4
图4 LED 电路图(一)

这里我们要学会看电路图的一个知识点,电路图右侧所有的 LED 下侧的线最终都连到一根黑色的粗线上去了,大家注意,这个地方不是实际的完全连到一起,而是一种总线的画法,画了这种线以后,表示这是个总线结构。

而所有的名字一样的节点是一一对应的连接到一起,其他名字不一样的,是不连在一起的。比如左侧的 DB0 和右侧的最右边的 LED2 小灯下边的DB0 是连在一起的,而和 DB1 等其他线不是连在一起的。

那么我们把图4中现在需要讲解的这部分单独摘出来看,如图5所示。

“图5
图5 LED 电路图(二)

现在我们通过5的电路图来计算一下,5V 的电压减去 LED 本身的压降,减掉三极管e 和 c 之间的压降,限流电阻用的是 330 欧,那么每条支路的电流大概是 8mA,那么 8 路 LED如果全部同时点亮的话电流总和就是 64mA。这样如果直接接到单片机的 IO 口,那单片机肯定是承受不了的,即使短时间可以承受,长时间工作就会不稳定,甚至导致单片机烧毁。

有的同学会提出来可以加大限流电阻的方式来降低这个电流。比如改到 1K,那么电流不到 3mA,8 路总的电流就是 20mA 左右。

首先,降低电流会导致 LED 小灯亮度变暗,小灯的亮度可能关系还不大,但因为我们同样的电路接了数码管,后边我们要讲数码管还要动态显示,如果数码管亮度不够的话,那视觉效果就会很差,所以降低电流的方法并不可取。

其次,对于单片机来说,他主要是起到控制作用,电流输入和输出的能力相对较弱,P0 的 8 个口总电流也有一定限制,所以如果接一两个 LED 小灯观察,可以勉强直接用单片机的 IO 口来接,但是接多个小灯,从实际工程的角度去考虑,就不推荐直接接 IO 口了。那么我们如果要用单片机控制多个 LED 小灯该怎么办呢?

除了三极管之外,其实还有一些驱动 IC,这些驱动 IC 可以作为单片机的缓冲器,仅仅是电流驱动缓冲,不起到任何逻辑控制的效果,比如我们板子上用的 74HC245 这个芯片,这个芯片在逻辑上起不到什么别的作用,就是当做电流缓冲器的,我们通过查看其数据手册,74HC245 稳定工作在 70mA 电流是没有问题的,比单片机的 8 个 IO 口大多了,所以我们可以把他接在小灯和 IO 口之间做缓冲,如图6所示。

“图6
图6 74HC245 功能图

从图6我们来分析,其中 VCC 和 GND 就不用多说了,细心的同学会发现这里有个0.1uF 的去耦电容哦。

74HC245 是个双向缓冲器,1 引脚 DIR 是方向引脚,当这个引脚接高电平的时候,右侧所有的 B 编号的电压都等于左侧 A 编号对应的电压。比如 A1 是高电平,那么 B1 就是高电平,A2 是低电平,B2 就是低电平等等。如果 DIR 引脚接低电平,得到的效果是左侧 A 编号的电压都会等于右侧 B 编号对应的电压。

因为我们这个地方控制端是左侧接的是 P0 口,我们要求 B 等于 A 的状态,所以 1 脚我们直接接的 5V 电源,即高电平。图 3-13 中还有一排电阻 R10 到 R17 是上拉电阻,这个电阻的用法我们在后边介绍。

还有最后一个使能引脚 19 脚 OE,叫做输出使能,这个引脚上边有一横,表明是低电平有效,当接了低电平后,74HC245 就会按照刚才上边说的起到双向缓冲器的作用,如果 OE接了高电平,那么无论 DIR 怎么接,A 和 B 的引脚是没有关系的,也就是 74HC245 功能不能实现出来。

从下面的图7可以看出来,单片机的 P0 口和 74HC245 的 A 端是直接接起来的。

这个地方,有个别同学有个疑问,就是我们明明在电源 VCC 那地方加了一个三极管驱动了,为何还要再加 245 驱动芯片呢?

这里大家要理解一个道理,电路上从正极经过器件到地,首先必须有电流才能正常工作,电路中任何一个位置断开,都不会有电流,器件也就不会参与工作了。

其次,和水流一个道理,从电源正极到负极的电流水管的粗细都要满足要求,任何一个位置的管子过细,都会出现瓶颈效应,电流在整个通路中细管处会受到限制而降低,所以在电路通路的每个位置上,都要保证通道足够畅通,这个 74HC245 的作用就是消除单片机IO 这一环节的瓶颈。

“图7
图7 单片机与 74HC245 的连接

本文转载自:网络
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 664

今天分享一篇单片机程序框架的文章。

程序架构重要性

很多人尤其是初学者在写代码的时候往往都是想一点写一点,最开始没有一个整体的规划,导致后面代码越写越乱,bug不断。

最终代码跑起来看似没有问题(有可能也真的没有问题),但是要加一个功能的时候会浪费大量的时间,甚至导致整个代码的崩溃。

所以,在一个项目开始的时候多花一些时间在代码的架构设计上是十分有必要的。代码架构确定好了之后你会发现敲代码的时候会特别快,并且在后期调试的时候也不会像无头苍蝇一样胡乱找问题。当然,调试也是一门技术。

在学习实时操作系统的过程中,发现实时操作系统框架与个人的业务代码之间的耦合性就非常低,都是只需要将业务代码通过一定的接口函数注册好后就交给操作系统托管了,十分方便。

但是操作系统的调度过于复杂,这里就使用操作系统的思维方式来重构这个时间片轮询框架。实现该框架的完全解耦,用户只需要包含头文件,并且在使用过程中不需要改动已经写好的库文件。

Demo

首先来个demo,该demo是使用电脑开两个线程:一个线程模拟单片机的定时器中断产生时间片轮询个时钟,另一个线程则模拟主函数中一直运行的时间片轮询调度程序。

#include <thread>
#include <stdio.h>
#include <windows.h>
#include "timeslice.h"

// 创建5个任务对象
TimesilceTaskObj task_1, task_2, task_3, task_4, task_5;

// 具体的任务函数
void task1_hdl()
{
    printf(">> task 1 is running ...\n");
}

void task2_hdl()
{
    printf(">> task 2 is running ...\n");
}

void task3_hdl()
{
    printf(">> task 3 is running ...\n");
}

void task4_hdl()
{
    printf(">> task 4 is running ...\n");
}

void task5_hdl()
{
    printf(">> task 5 is running ...\n");
}

// 初始化任务对象,并且将任务添加到时间片轮询调度中
void task_init()
{
    timeslice_task_init(&task_1, task1_hdl, 1, 10);
    timeslice_task_init(&task_2, task2_hdl, 2, 20);
    timeslice_task_init(&task_3, task3_hdl, 3, 30);
    timeslice_task_init(&task_4, task4_hdl, 4, 40);
    timeslice_task_init(&task_5, task5_hdl, 5, 50);
    timeslice_task_add(&task_1);
    timeslice_task_add(&task_2);
    timeslice_task_add(&task_3);
    timeslice_task_add(&task_4);
    timeslice_task_add(&task_5);
}


// 开两个线程模拟在单片机上的运行过程
void timeslice_exec_thread()
{
    while (true)
    {
        timeslice_exec();
    }
}

void timeslice_tick_thread()
{
    while (true)
    {
        timeslice_tick();
        Sleep(10);
    }
}

int main()
{
    task_init();

    printf(">> task num: %d\n", timeslice_get_task_num());
    printf(">> task len: %d\n", timeslice_get_task_timeslice_len(&task_3));

    timeslice_task_del(&task_2);
    printf(">> delet task 2\n");
    printf(">> task 2 is exist: %d\n", timeslice_task_isexist(&task_2));

    printf(">> task num: %d\n", timeslice_get_task_num());

    timeslice_task_del(&task_5);
    printf(">> delet task 5\n");

    printf(">> task num: %d\n", timeslice_get_task_num());

    printf(">> task 3 is exist: %d\n", timeslice_task_isexist(&task_3));
    timeslice_task_add(&task_2);
    printf(">> add task 2\n");
    printf(">> task 2 is exist: %d\n", timeslice_task_isexist(&task_2));

    timeslice_task_add(&task_5);
    printf(">> add task 5\n");

    printf(">> task num: %d\n", timeslice_get_task_num());

    printf("\n\n========timeslice running===========\n");

    std::thread thread_1(timeslice_exec_thread);
    std::thread thread_2(timeslice_tick_thread);

    thread_1.join();
    thread_2.join();


    return 0;
}

运行结果如下:

“单片机面向对象思维的架构:时间轮片法"

由以上例子可见,这个框架使用十分方便,甚至可以完全不知道其原理,仅仅通过几个简单的接口就可以迅速创建任务并加入到时间片轮询的框架中,十分好用。

时间片轮询架构

其实该部分主要使用了面向对象的思维,使用结构体作为对象,并使用结构体指针作为参数传递,这样作可以节省资源,并且有着极高的运行效率。

其中最难的部分是侵入式链表的使用,这种链表在一些操作系统内核中使用十分广泛,这里是参考RT-Thread实时操作系统中的侵入式链表实现。

h文件:

#ifndef _TIMESLICE_H
#define _TIMESLICE_H

#include "./list.h"

typedef enum {
    TASK_STOP,
    TASK_RUN
} IsTaskRun;

typedef struct timesilce
{
    unsigned int id;
    void (*task_hdl)(void);
    IsTaskRun is_run;
    unsigned int timer;
    unsigned int timeslice_len;
    ListObj timeslice_task_list;
} TimesilceTaskObj;

void timeslice_exec(void);
void timeslice_tick(void);
void timeslice_task_init(TimesilceTaskObj* obj, void (*task_hdl)(void), unsigned int id, unsigned int timeslice_len);
void timeslice_task_add(TimesilceTaskObj* obj);
void timeslice_task_del(TimesilceTaskObj* obj);
unsigned int timeslice_get_task_timeslice_len(TimesilceTaskObj* obj);
unsigned int timeslice_get_task_num(void);
unsigned char timeslice_task_isexist(TimesilceTaskObj* obj);

#endif

c文件:

#include "./timeslice.h"

static LIST_HEAD(timeslice_task_list);

void timeslice_exec()
{
    ListObj* node;
    TimesilceTaskObj* task;

    list_for_each(node, &timeslice_task_list)
    {
           
        task = list_entry(node, TimesilceTaskObj, timeslice_task_list);
        if (task->is_run == TASK_RUN)
        {
            task->task_hdl();
            task->is_run = TASK_STOP;
        }
    }
}

void timeslice_tick()
{
    ListObj* node;
    TimesilceTaskObj* task;

    list_for_each(node, &timeslice_task_list)
    {
        task = list_entry(node, TimesilceTaskObj, timeslice_task_list);
        if (task->timer != 0)
        {
            task->timer--;
            if (task->timer == 0)
            {
                task->is_run = TASK_RUN;
                task->timer = task->timeslice_len;
            }
        }
    }
}

unsigned int timeslice_get_task_num()
{
    return list_len(&timeslice_task_list);
}

void timeslice_task_init(TimesilceTaskObj* obj, void (*task_hdl)(void), unsigned int id, unsigned int timeslice_len)
{
    obj->id = id;
    obj->is_run = TASK_STOP;
    obj->task_hdl = task_hdl;
    obj->timer = timeslice_len;
    obj->timeslice_len = timeslice_len;
}

void timeslice_task_add(TimesilceTaskObj* obj)
{
    list_insert_before(&timeslice_task_list, &obj->timeslice_task_list);
}

void timeslice_task_del(TimesilceTaskObj* obj)
{
    if (timeslice_task_isexist(obj))
        list_remove(&obj->timeslice_task_list);
    else
        return;
}


unsigned char timeslice_task_isexist(TimesilceTaskObj* obj)
{
    unsigned char isexist = 0;
    ListObj* node;
    TimesilceTaskObj* task;

    list_for_each(node, &timeslice_task_list)
    {
        task = list_entry(node, TimesilceTaskObj, timeslice_task_list);
        if (obj->id == task->id)
            isexist = 1;
    }

    return isexist;
}

unsigned int timeslice_get_task_timeslice_len(TimesilceTaskObj* obj)
{
    return obj->timeslice_len;
}

底层侵入式双向链表

该链表是linux内核中使用十分广泛,也十分经典,其原理具体可以参考文章:
https://www.cnblogs.com/skywang12345/p/3562146.html

h文件:

#ifndef _LIST_H
#define _LIST_H

#define offset_of(type, member)             (unsigned long) &((type*)0)->member
#define container_of(ptr, type, member)     ((type *)((char *)(ptr) - offset_of(type, member)))

typedef struct list_structure
{
    struct list_structure* next;
    struct list_structure* prev;
} ListObj;

#define LIST_HEAD_INIT(name)    {&(name), &(name)}
#define LIST_HEAD(name)         ListObj name = LIST_HEAD_INIT(name)

void list_init(ListObj* list);
void list_insert_after(ListObj* list, ListObj* node);
void list_insert_before(ListObj* list, ListObj* node);
void list_remove(ListObj* node);
int list_isempty(const ListObj* list);
unsigned int list_len(const ListObj* list);

#define list_entry(node, type, member) \
    container_of(node, type, member)

#define list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)

#define list_for_each_safe(pos, n, head) \
  for (pos = (head)->next, n = pos->next; pos != (head); \
    pos = n, n = pos->next)

#endif

c文件:

#include "list.h"

void list_init(ListObj* list)
{
    list->next = list->prev = list;
}

void list_insert_after(ListObj* list, ListObj* node)
{
    list->next->prev = node;
    node->next = list->next;

    list->next = node;
    node->prev = list;
}

void list_insert_before(ListObj* list, ListObj* node)
{
    list->prev->next = node;
    node->prev = list->prev;

    list->prev = node;
    node->next = list;
}

void list_remove(ListObj* node)
{
    node->next->prev = node->prev;
    node->prev->next = node->next;

    node->next = node->prev = node;
}

int list_isempty(const ListObj* list)
{
    return list->next == list;
}

unsigned int list_len(const ListObj* list)
{
    unsigned int len = 0;
    const ListObj* p = list;
    while (p->next != list)
    {
        p = p->next;
        len++;
    }

    return len;
}

到此,一个全新的,完全解耦的,十分方便易用时间片轮询框架完成。

来源:ERYUESANHI
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 101

在单片机开发中,UART、I2C、RS485等普遍在用,对它们的认识可能模棱两可,本文把它们整理了一下。本文较长,阅读时间大约10分钟。

UART通用异步收发器

UART口指的是一种物理接口形式(硬件)。

“单片机常用的几种通信接口"

UART是异步,全双工串口总线。它比同步串口复杂很多。有两根线,一根TXD用于发送,一根RXD用于接收。

UART的串行数据传输不需要使用时钟信号来同步传输,而是依赖于发送设备和接收设备之间预定义的配置。

对于发送设备和接收设备来说,两者的串行通信配置应该设置为完全相同。

“单片机常用的几种通信接口"

起始位:表示数据传输的开始,电平逻辑为“0” 。

数据位:可能值有5、6、7、8、9,表示传输这几个bit 位数据。一般取值为8,因为一个ASCII 字符值为8 位。

奇偶校验位:用于接收方对接收到的数据进行校验,校验“1” 的位数为偶数(偶校验) 或奇数(奇校验),以此来校验数据传送的正确性,使用时不需要此位也可以。

停止位:表示一帧数据的结束。电平逻辑为“1”。

如果用通用IO口模拟UART总线,则需一个输入口,一个输出口。

I2C总线

I2C总线是一种同步、半双工双向的两线式串口总线。它由两条总线组成:串行时钟线SCL和串行数据线SDA。

SCL线——负责产生同步时钟脉冲。

SDA线——负责在设备间传输串行数据。

该总线可以将多个I2C设备连接到该系统上。连接到I2C总线上的设备既可以用作主设备,也可以用作从设备。

“单片机常用的几种通信接口"

主设备负责控制通信,通过对数据传输进行初始化,来发送数据并产生所需的同步时钟脉冲。从设备则是等待来自主设备的命令,并响应命令接收。

主设备和从设备都可以作为发送设备或接收设备。无论主设备是作为发送设备还是接收设备,同步时钟信号都只能由主设备产生。

如果用通用IO口模拟I2C总线,并实现双向传输,则需一个输入输出口(SDA),另外还需一个输出口(SCL)。

SPI串行外设接口

SPI总线是同步、全双工双向的4线式串行接口总线。它是由“单个主设备+多个从设备”构成的系统。

在系统中,只要任意时刻只有一个主设备是处于激活状态的,就可以存在多个SPI主设备。常运用于AD转换器、EEPROM、FLASH、实时时钟、数字信号处理器和数字信号解码器之间实现通信。

“单片机常用的几种通信接口"

为了实现通信,SPI共有4条信号线,分别是:

  • 主设备出、从设备入(Master Out Slave In,MOSI):由主设备向从设备传输数据的信号线,也称为从设备输入(Slave Input/Slave Data In,SI/SDI)。

  • 主设备入、从设备出(Master In Slave Out,MISO):由从设备向主设备传输数据的信号线,也称为从设备输出(Slave Output/Slave Data Out,SO/SDO)。

  • 串行时钟(Serial Clock,SCLK):传输时钟信号的信号线。

  • 从设备选择(Slave Select,SS):用于选择从设备的信号线,低电平有效。

SPI 的工作时序模式由CPOL(Clock Polarity,时钟极性)和CPHA(Clock Phase,时钟相位)之间的相位关系决定,CPOL 表示时钟信号的初始电平的状态,CPOL 为0 表示时钟信号初始状态为低电平,为1 表示时钟信号的初始电平是高电平。CPHA 表示在哪个时钟沿采样数据,CPHA 为0 表示在首个时钟变化沿采样数据,而CPHA 为1 则表示在第二个时钟变化沿采样数据。

“单片机常用的几种通信接口"

UART、SPI、I2C比较

  • I2C线更少,比UART、SPI更为强大,但是技术上也更加麻烦些,因为I2C需要有双向IO的支持,而且使用上拉电阻,抗干扰能力较弱,一般用于同一板卡上芯片之间的通信,较少用于远距离通信。

  • SPI实现要简单一些,UART需要固定的波特率,就是说两位数据的间隔要相等,而SPI则无所谓,因为它是有时钟的协议。

  • I2C的速度比SPI慢一点,协议比SPI复杂一点,但是连线也比标准的SPI要少。

  • UART一帧可以传5/6/7/8位,I2C必须是8位。I2C和SPI都从最高位开始传。

  • SPI用片选信号选择从机,I2C用地址选择从机。

“单片机常用的几种通信接口"

RS232串口通信

传输线有两根,地线一根。电平是负逻辑:

-3V~-15V逻辑“1”,+3V~+15V逻辑“0”。

RS-232串口通信传输距离15米左右。可做到双向传输,全双工通讯,传输速率低20kbps 。

下图是DB9公头和母头的定义,一般用的最多的是RXD、TXD、GND三个信号。

“单片机常用的几种通信接口"

TTL和RS-232互转

单片机接口一般是TTL电平,如果接232电平的外设,就需要加TTL转RS232的模块。如下图,可用芯片MAX232进行转换。

“单片机常用的几种通信接口"

RS422串口通信

RS-422有4根信号线:两根发送、两根接收和一根地线,是全双工通信。

它有一个主设备,其余为从设备,从设备之间不能通信,所以RS-422支持点对多的双向通信。

“单片机常用的几种通信接口"

RS485串口通信

RS-485采用平衡发送和差分接收,因此具有抑制共模干扰的能力。

采用两线半双工传输,最大速率10Mb/s,电平逻辑是两线的电平差来决定的,提高抗干扰能力,传输距离长(几十米到上千米)。

+2V~+6V逻辑“1”,-2~-6V逻辑“0”。

TTL转成RS-485很常见,比如MAX485,参考电路如下

“单片机常用的几种通信接口"

RE引脚:接收器输出使能(低电平有效)。

DE引脚:发送器输出使能(高电平有效)。可以直接通过MCU的IO端口控制。

TTL

嵌入式里面说的串口,一般是指UART口。4个pin(Vcc,GND,RX,TX),用TTL电平。

PC中的COM口即串行通讯端口,简称串口。9个Pin,用RS232电平。

“单片机常用的几种通信接口"

串口、COM口是指的物理接口形式(硬件)。而TTL、RS-232、RS-485是指电平标准(电信号)。

“单片机常用的几种通信接口"

单片机与PC通讯示意图如下:

“单片机常用的几种通信接口"

CAN总线

CAN是控制器局域网络的简称,是一种能够实现分布式实时控制的串行通信网络。CAN总线的功能复杂且智能。主要用于汽车通信。

CAN总线网络主要挂在CAN_H和CAN_L,各个节点通过这两条线实现信号的串行差分传输,为了避免信号的反射和干扰,还需要在CAN_H和CAN_L之间接上120欧姆的终端电阻。

“单片机常用的几种通信接口"

每一个设备既可做主设备也可做从设备。CAN总线的通信距离可达10千米(速率低于5Kbps),速度可达1Mbps(通信距离小于40M)。

“单片机常用的几种通信接口"

CAN电平逻辑

CAN总线采用"线与"的规则进行总线冲裁,1&0为0,所以称0为显性,1为隐性。

从电位上看,因为规定高电位为0,低电位为1,同时发出信号时实际呈现为高电位,从现象上看就像0覆盖了1,所以称0为显性,1为隐性。

“单片机常用的几种通信接口"

USB通信串行总线

USB接口最少有四根线,其中有两根是数据线,而所有的USB数据传输都是通过这两根线完成。它的通信远比串口复杂的多。

两根数据线采用差分传输,即需要两根数据线配合才能传输一个bit,因此是半双工通信,同一时间只能发送或者接收。

USB 规定,如果电压电平不变,代表逻辑1;如果电压电平变化,则代表逻辑0。

“单片机常用的几种通信接口"

USB转TTL

一般USB转串口都是用CH340G芯片。

“单片机常用的几种通信接口"

用串口通信比USB简单,因为串口通信没有协议。

SD卡

SD卡是一种存储卡,可用于手机作为内存卡使用。

嵌入式中,单片机与SD卡通信有两种模式:

  • SPI总线通信模式

  • SD总线通信模式

“单片机常用的几种通信接口"

值得注意的是,SD总线模式中有4条数据线;SPI总线模式中仅有一条数据线(MOSI和MISO不能同时读数据,也不能同时写数据);这样在嵌入式中,单片机与SD卡通信时采用SD总线模式比SPI总线模式速度快几倍。

“单片机常用的几种通信接口"

1-WIRE总线

1-Wire由美国Dallas(达拉斯)公司推出,是一种异步半双工串行传输。采用单根信号线,既传输时钟又传输数据,而且数据传输是双向的。

“单片机常用的几种通信接口"

单总线的数据传输速率一般为16.3Kbit/s,最大可达142 Kbit/s,通常情况下采用100Kbit/s以下的速率传输数据。

1-Wire线端口为漏极开路或三态门的端口,因此一般需要加上拉电阻Rp,通常选用5K~10KΩ

主要应用在:打印墨盒或医疗消耗品的识别;印刷电路板、配件及外设的识别和认证。

DMA直接存储器访问

DMA是STM32内的一个硬件模块,它独立于CPU,在外围设备和内存之间进行数据传输,解放了CPU,可使CPU的效率大大提高。

“单片机常用的几种通信接口"

它可以高速访问外设、内存,传输不受CPU的控制,并且是双向通信。因此,使用DMA可以大大提高数据传输速度,这也是ARM架构的一个亮点——DMA总线控制。

DMA就相应于一条高速公路,专用、高速的特性。如果不使用DMA,也可以达到目的,只是达到目的的时间比较长。

Ethernet以太网

以太网是目前应用最普遍的局域网技术。

大家知道,以太网接口可分为协议层和物理层。

协议层是由一个叫MAC(Media Access Layer)控制器的单一模块实现。

物理层由两部分组成,即PHY(Physical Layer)和传输器。

目前很多主板的南桥芯片已包含了以太网MAC控制功能,只是未提供物理层接口。因此,需外接PHY芯片以提供以太网的接入通道。

“单片机常用的几种通信接口"

网络变压器的作用是:

  • 耦合差分信号,抗干扰能力更强

  • 变压器隔离网线端不同设备的不同电平,隔离直流信号

以太网接口参考电路,如下图所示。

“单片机常用的几种通信接口"

来源:网络
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 3942

页面

订阅 RSS - 单片机