英创信息技术低成本多通道波形采集显示方案的软件实现方法

描述

英创ARM9系列嵌入式主板EM9170加上新近推出的数据采集扩展模块ETA108,可实现低成本的多通道波形采集。该方案的硬件平台见如下文章:《低成本多通道波形采集方案》。本文将从应用的角度详细介绍ETA108接口的使用方法,并在此基础之上实现一个完整的多通道波形采集的图形界面显示方案。

该方案采用EM9170嵌入式主板,及扩展模块ETA108,可同时采集8个通道,最高采样频率100KHz,AD转换精度12bit。图形界面在7寸屏上全屏显示,以波形图形式同时显示各个通道AD采样结果。用户可以自由移动观察波形,并通过鼠标指针获得波形图中各点详细信息。

下图为ETA108波形采集程序对一路正旋波和一路三角波的AD采样截图,采样频率为50000Hz。

本文介绍该方案中ETA108模块参数,驱动安装,接口调用方法,以及使用C++编程,实现图形界面显示的一些程序开发方法。

1、ETA108模块参数

ETA108是为了进一步支持EM9170在仪器仪表,数据采集领域应用,同时也是为了方便客户使用而推出的一款低成本高性能AD采集模块。ETA108的主要性能如下:

• 8通道单端输入或4通道差分输入

• 单极性输入量程0~4V或双极性输入量程±2V

• 每通道具有独立的高阻抗增益放大器(PGA),可实现各种传感器之间的直接接口连接,并支持用户配置通道增益(Gain=1/2/4/8)

• AD转换精度12bit

• AD最高采样速度100ksps

• 可选择多种平均操作模式,使输出AD精度达到14bit

• 单5V供电

关于ETA108的详细介绍,可阅读ETA108的手册:《ETA108数据采集模块使用手册》。

2、驱动安装

复制ETA108驱动程序安装包Emtronix ETA108.cab到EM9170主板NandFlash目录下。

安装该文件到NandFlash目录下。

WINDOWS

安装完成后在NandFlash目录下会生成驱动文件ETA108V2.dll。安装完成之后,断电重启不需要重复安装。

3、ETA108模块调用方法

请参考产品光盘中ETA108的测试例程,在工程中添加ETA108.h和ETA108.cpp文件,并添加对应的引用,即可非常方便的使用API控制ETA108模块。

ETA108驱动程序提供的接口函数说明如下:

(1) BOOL ETA108Open( )
功能描述:调用CreateFile函数,打开ETA108驱动程序
返回值:=TRUE:打开ETA108成功    = FALSE:打开失败

(2) BOOL Setup( PADS_CONFIG pADSConfig, PADS_CONFIG pADSConfigOut )
功能描述:设置AD采集相关参数,采集通道,采样长度等
输入参数:pADSConfig 配置参数结构体指针
输出参数:pADSConfigOut
返回值:=TRUE:参数设置成功    =FALSE:参数设置失败

ADSConfig结构体是ETA108的配置数据结构体,包含了采样率,采样长度,采样通道设置,通道寄存器配置等参数。其定义如下:

typedef struct
{
        DWORD dwSamplingRate;
        DWORD dwSamplingLength;
        DWORD dwSamplingChannel;
        LPVOID lpContrlWord;
        DWORD dwContrlWordLength;
} ADS_CONFIG, *PADS_CONFIG;

ADS_CONFIG结构体即可用为函数的输入参考,也可作为输出参数使用,其结构体成员含义说明如下:

成员 输入参数定义 输出参数定义
  dwSamplingRate   设置每个AD通道的采样率   返回总的采样率(=每通道采样率*采样通道数)
  dwSamplingLength   设置每个AD通道的采样长度
  >0:单次采样  =0:连续采样
  返回总的采样长度(=每通道采长度*采样通道数)
  dwSamplingChannel*   设置需要采样的通道   返回采样的通道数
  lpContrlWord   指向AD通道配置的buffer,此参数用于设置ETA108的
  寄存器,lpContrlWord =NULL时,系统使用默认配置
 
  dwContrlWordLength   lpContrlWord 指向buffer的长度  

*dwSamplingChannel的低8bit(bit0~bit7)依次对应AD通道0~通道7,如果要采集某个通道的数据,需要将其对应的位置为1。比如要采集通道0、通道1和通道7的数据,则应设置dwSamplingChannel=0x83。

(3) BOOL Start( )
功能描述:启动AD采集
返回值:=TRUE 开始AD采集    =FALSE 启动AD采集

(4) BOOL WaitDataReady( DWORD dwTimeOut )
功能描述:等待AD采集完成
输入参数:dwTimeOut 等待超时时间(ms),设置dwTimeOut=0时,驱动程序将自动计算一个合适的等待时间。连续采样模式下,此函数大约250ms返回一次
返回值:=TRUE AD采集完成,接下来可读取采集数据    =FALSE 等待超时,AD采集存在错误

(5) DWORD Read( LPVOID pBuf, DWORD dwReadLength )
功能描述:读取采集数据
输入参数:pBuf 用于存放数据的buffer    dwReadLength 要读取的数据个数(以字节计数)即采样长度×采样通道数×sizeof(UINT32)
返回值:>0 实际读取的字节数    =0 无采集数据    =-1 函数执行失败

(6) BOOL Stop( )
功能描述:中止当前AD采集。
返回值:=TRUE 函数执行成功    =FALSE 函数执行失败

(7) void ETA108Close ( )
功能描述:关闭ETA108,释放相关资源

4、采样结果

使用Read函数,传入32bit数组指针。该数组长度为ADSConfigOut. dwSamplingChannel,即采样长度×采样通道数。获得的采样数据在数组中按各通道依次排列。

例如:pBuf为数组指针,采样通道为AD3和AD5,采样长度为5000,那么pBuf长度为10000,即5000*2*sizeof(UINT32)字节,其中AD3的5000长度数据依次放在pBuf[0]到pBuf[4999],AD5的5000长度数据依次放在pBuf[5000]到pBuf[9999]。

数组中每一位32bit数据具体定义如下:

WINDOWS

WINDOWS

其中第0位是单端/差分标识位,第1-3位是通道地址位,第4、5位平均模式下增加的2位分辨率,第6-17为12bit的AD数据。

例如:获得pBuf中第n位的AD数据值v,即v = pBuf[n]>>6。

理想情况下,输入电压与AD输出的12bit数据定义如下:

描述 模拟量输入 二进制数字输出 十六进制数字输出
  满量程范围   V¬REF¬    
  最小分辨率
(LSB)
  V¬REF/4096¬    
  满量程   V¬REF-1LSB   1111 1111 1111   FFFF
  1/2量程   V¬REF/2   1000 0000 0000   8000
  1/2量程-1LSB   V¬REF/2-1LSB   0111 1111 1111   7FFF
  零   0V   0000 0000 0000   0000

5、连续采样

设置dwSamplingLength=0时,ETA108工作在连续采样模式。在连续采样模式下,驱动程序连续不断的进行数据采集,并大约每隔250ms通知一次应用程序,以便应用程序可将数据从驱动缓存中读出。应用程序可从Setup函数的输出参数:ADS_CONFIG结构体的dwSamplingLength成员,得到每次可以读取的数据总长度。

WINDOWS

6、波形采集程序说明

ETA108波形采集例程实现了单次采样的操作,并将采样的波形绘制成曲线,显示在界面上。

(1)在对话框初始化函数OnInitDialog中初始各个参数和界面设置,然后打开ETA108设备。

// 加载驱动
if (!ETA108Open())
{
        AfxMessageBox(L'打开设备失败');
}

(2)在“采集”按钮的响应函数中,根据界面中选择的采样频率,采样长度,采样通道,将参数设置到配制结构体ADSConfig中,执行Setup函数设置参数。

ADS_CONFIG adsConfig; // 采样参数设置
ADS_CONFIG adsConfigOut; // 返回采样参数
adsConfig.dwSamplingRate = m_dwSamplingRate;
adsConfig.dwSamplingLength = m_dwSamplingLength;
adsConfig.dwSamplingChannel = m_dwSamplingChannel;

ret = Setup(&adsConfig,&adsConfigOut);
if (!ret)
{
        AfxMessageBox(L'参数设置失败');
        return;
}

(3)根据采样长度申请一段数组存储AD数据。


UINT32* pBuf; // 接收区指针
if(pBuf != NULL)
{
        delete [] pBuf;
        pBuf = NULL;
}
pBuf = new UINT32[adsConfigOut.dwSamplingLength];
if( pBuf == NULL )
{
        AfxMessageBox(L'创建BUFFER失败');
        return;
}

(4)执行Start函数开始采集。


ret = Start();
if (!ret)
{
        AfxMessageBox(L'执行失败');
        return;
}

(5)执行等待函数WaitDataReady等待ETA108采样完成。


ret = WaitDataReady(0);

(6)在等待完成后执行Read函数读出AD采样结果。


DWORD dwNumberOfBytesRead;
dwNumberOfBytesRead = Read( pBuf, adsConfigOut.dwSamplingLength*sizeof(UINT32));
if (dwNumberOfBytesRead == -1)
{
        AfxMessageBox(L'读取失败');
        return;
}

(7)处理BUF内的数据,然后根据实际需要将数据存放数据库,或是曲线形式显示在界面上。例程中根据采样长度设置滚动条,并执行绘制曲线函数DrawCurve。

(8)程序关闭函数中,释放资源,关闭ETA108。


ETA108Close();

7、界面设计

本波形采集程序使用MFC的对话框程序,绘图部分使用的GDI函数。程序界面对应7寸屏,界面大小为800×480。

(1)全屏显示,隐藏WINCE系统工具栏,设置对话框大小为800×480,然后在对话框初始函数中添加代码

方案一:

1.然后使用API函数获得工具栏窗口句柄。
HWND hWnd = ::FindWindow(L'HHTaskBar', NULL);

2.重新设置,隐藏窗口。
::ShowWindow(hWnd, SW_MINIMIZE);

这个方法在关闭程序后工具栏不会恢复,并且每次重启后,工具栏会恢复。如果设置程序为自启动,希望在系统启动到打开程序期间不会让用户看到弹出的工具栏,可以使用方案二。

方案二:

1.设置工具栏自动隐藏,并设置工具栏不会总在最上方,该操作可以右键工具栏->属性,进行设置。

WINDOWS

代码实现为修改注册表,然后通知工具栏更新。

/*BOOL bAutoHide = TRUE;
lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, L'Software\Microsoft\Shell\AutoHide', 0, KEY_ALL_ACCESS, &hkey);
if(lRet != ERROR_SUCCESS) {
        lRet = RegCreateKeyEx(HKEY_LOCAL_MACHINE, L'Software\Microsoft\Shell\AutoHide', 0, NULL, 0, KEY_ALL_ACCESS,
NULL, &hkey, &dw);
}
if (lRet == ERROR_SUCCESS) {
        RegSetValueEx(hkey, L'', 0, REG_DWORD, (LPBYTE)&bAutoHide, sizeof(DWORD));
        RegCloseKey(hkey);
}*/
BOOL bOnTop = FALSE;
lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, L'Software\Microsoft\Shell\OnTop', 0, KEY_ALL_ACCESS, &hkey);
if(lRet != ERROR_SUCCESS) {
        lRet = RegCreateKeyEx(HKEY_LOCAL_MACHINE, L'Software\Microsoft\Shell\OnTop', 0, NULL, 0, KEY_ALL_ACCESS,
NULL, &hkey, &dw);
}
if (lRet == ERROR_SUCCESS) {
        RegSetValueEx(hkey, L'', 0, REG_DWORD, (LPBYTE)&bOnTop, sizeof(DWORD));
        RegCloseKey(hkey);
}
::PostMessage(hWnd, WM_WININICHANGE, 0, 5000);

(2)程序背景设计

MFC自带的边框和标题栏颜色比较单调,另外其他控件也过于朴实。所以例程里将窗体的Border属性设置为None,然后以图片背景的形式设计程序的标题栏,及其他控件的背景。

使用绘图软件,如Photoshop根据实际应用状况,制作一副800×480的界面图。

根据界面图调整各个控件的位置,MFC的界面设计如下。

WINDOWS

将背景图片添加到资源文件中,然后新建背景画刷,修改对话框的OnCtlColor函数,在重刷对话框的时候,返回背景画刷。

Brush brBk;
BOOL CETA108_TESTDlg::OnInitDialog()
{
        …
        CBitmap bmp;
        bmp.LoadBitmap(IDB_BITMAP_BK);
        brBk.CreatePatternBrush(&bmp);
        bmp.DeleteObject();
        …
}

HBRUSH CETA108_TESTDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
        HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
        // TODO: Change any attributes of the DC here
        if (pWnd == this)
        {
                return brBk;
        }
        …
}

(3)自定义按钮的实现

静态控件可以都放在背景图片中,按钮需要另外制作。

1、同样在Photoshop中设计好按钮各个状态的图片。这里有两个按钮,一个是“开始采样”按钮,一个是“退出”按钮。
其中“开始采样”按钮设计3个状态。

WINDOWS

普通状态                高亮状态                按下状态

“退出”按钮设计2个状态。

WINDOWS 
普通状态                高亮状态

将这些图片添加到资源中。

2、添加一个基于MFC普通按钮CButton的新类CMyButton。

在对话框界面中右键->添加类,选择基类为CButton,添加一个新类CMyButton。

// 给这个类添加储存各个状态位图的CBitmap成员
CBitmap m_bmpBtnnormal; // 普通
CBitmap m_bmpBtnon; // 高亮
CBitmap m_bmpBtndown; // 按下
// 添加一个成员记录按键是否处于鼠标位置(高亮状态)
BOOL m_bTracking;

3、在对话框中添加自定义按钮,设置他们为CMyButton。

CMyButton m_BtnBegin;
CMyButton m_BtnExit;

4、给自定义按钮添加消息响应函数。

如果按钮要实现鼠标移动上去显示高亮的效果,需要给CMyButton类添加消息响应。如果鼠标移动到按键上,则设置m_bTracking = TRUE;当鼠标离开,则设置m_bTracking = FALSE;

Wince不同于Windows,CButton不能添加WM_MOUSEHOVER和WM_MOUSELEAVE消息,只能设置鼠标移动消息响应函数。

头文件添加
DECLARE_MESSAGE_MAP()
afx_msg void OnMouseMove(UINT nFlags, CPoint point);

cpp文件添加
BEGIN_MESSAGE_MAP(CMyButton, CButton)
ON_WM_MOUSEMOVE() // 必须在按钮上移动才有消息
// ON_MESSAGE(WM_MOUSEHOVER, OnMouseHover) // wince不行
// ON_MESSAGE(WM_MOUSELEAVE, OnMouseLeave)
END_MESSAGE_MAP()

void CMyButton::OnMouseMove(UINT nFlags, CPoint point)
{
        // TODO: 在此添加消息处理程序代码和/或调用默认值
        if (!m_bTracking)
        {
                m_bTracking = TRUE;
                Invalidate();
        }
        // CButton::OnMouseMove(nFlags, point);
}

按钮的鼠标移动消息是当鼠标在按钮上移动时才响应,所以在响应函数里直接设置m_bTracking = TRUE;而鼠标移开的消息只能在对话框的鼠标移动消息函数中来通知。

给CMyButton添加一个方法:
public:
void MouseLeave();
void CMyButton::MouseLeave() // WINCE控件响应不到MOUSE事件,只好DIALOG来通知了
{
        if (m_bTracking)
        {
                m_bTracking = FALSE;
                Invalidate();
        }
}

对话框的鼠标移动消息里面调用自定义按钮的MouseLeave方法:
void CETA108_TESTDlg::OnMouseMove(UINT nFlags, CPoint point)
{
        // TODO: Add your message handler code here and/or call default
        //加按钮处理
        m_BtnBegin.MouseLeave();
        m_BtnExit.MouseLeave();
        …
}

5、添加CMyButton类的初始化函数,在初始化函数中重新加载控件,并加载按钮图片。

BOOL CMyButton::InitButton(CDialog* pParent, UINT nCtlID, UINT nID1, UINT nID2, UINT nID3) // 在OnInitDialog()中初始化按钮上使用
{
        CWnd *pWnd = pParent->GetDlgItem(nCtlID); // 取得控件的指针
        if (pWnd)
        {
                pWnd->GetWindowRect(&m_rect); // 返回指定窗口的边框矩形的尺寸(屏幕坐标)
                pParent->ScreenToClient(&m_rect); // 以用户坐标替代屏幕坐标

                CString szCaption;
                pWnd->GetWindowText(szCaption); // 获取控件上已赋的文本
                DWORD dwStyle= pWnd->GetStyle(); // 获取控件原有的风格
                pWnd->DestroyWindow(); // 销毁控件
                Create(szCaption, dwStyle|BS_OWNERDRAW,m_rect, pParent, nCtlID); // 在控件的基础上创建一个新控件
               // 加载图片
               m_bmpBtnnormal.LoadBitmap(nID1);
               m_bmpBtnon.LoadBitmap(nID2);
               m_bmpBtndown.LoadBitmap(nID3);
               return TRUE;
        }
        else
        {
               return FALSE;
        }
}

在对话框的初始化函数中调用自定义按钮初始函数,传入对话框指针和按钮图片资源ID。

BOOL CETA108_TESTDlg::OnInitDialog()
{
        CDialog::OnInitDialog();
        // 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动
        // 执行此操作
        SetIcon(m_hIcon, TRUE); // 设置大图标
        SetIcon(m_hIcon, FALSE); // 设置小图标
        …
        // 自定义Button
        m_BtnBegin.InitButton(this, IDC_BUTTON1, IDB_BITMAP_BTNNORMAL, IDB_BITMAP_BTNON, IDB_BITMAP_BTNDOWN);
        m_BtnExit.InitButton(this, IDC_BUTTON2, IDB_BITMAP_BTN_EXIT_NORMAL, IDB_BITMAP_BTN_EXIT_ON, IDB_BITMAP_BTN_EXIT_NORMAL);
        …
        return TRUE; // 除非将焦点设置到控件,否则返回TRUE
}

6、在CMyButton的析构函数中释放图片资源。

CMyButton::~CMyButton()
{
        // 释放个图片资源
        if(m_bmpBtnnormal.GetSafeHandle() != NULL)
        {
                m_bmpBtnnormal.DeleteObject();
        }
        if(m_bmpBtnon.GetSafeHandle() != NULL)
        {
                m_bmpBtnon.DeleteObject();
        }
        if(m_bmpBtndown.GetSafeHandle() != NULL)
        {
                m_bmpBtndown.DeleteObject();
        }
}

7、重写CMyButton的DrawItem方法,实现自定义按钮的画图。

void CMyButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
        // TODO: 在此添加消息处理程序代码和/或调用默认值
        BOOL bSelected = FALSE;
        if (lpDrawItemStruct->itemState & ODS_SELECTED)
        {
                bSelected = TRUE;
        }
        else
        {
                bSelected = FALSE;
        }
        CDC DC;
        DC.Attach(lpDrawItemStruct->hDC);
        CDC dcMem;
        dcMem.CreateCompatibleDC(&DC);
        CBitmap* pOldBmp = NULL;
        CRect rcItem(lpDrawItemStruct->rcItem);
        BITMAP bp;
        if (bSelected) // 按下鼠标时的状态
        {
                pOldBmp = dcMem.SelectObject(&m_bmpBtndown);
                m_bmpBtndown.GetBitmap(&bp);
                DC.BitBlt(0,0,bp.bmWidth, bp.bmHeight,&dcMem,0,0,SRCCOPY);
        }
        else if (m_bTracking) // 鼠标置于按钮上,但未按下
        {
                pOldBmp = dcMem.SelectObject(&m_bmpBtnon);
                m_bmpBtnon.GetBitmap(&bp);
                DC.BitBlt(0,0,bp.bmWidth, bp.bmHeight,&dcMem,0,0,SRCCOPY);
        } 
        else // 鼠标未置于按钮上时的常态
        { 
                pOldBmp = dcMem.SelectObject(&m_bmpBtnnormal);
                m_bmpBtnnormal.GetBitmap(&bp);
                DC.BitBlt(0,0,bp.bmWidth,bp.bmHeight,&dcMem,0,0,SRCCOPY);
        }
        dcMem.SelectObject(pOldBmp);
        DC.Detach();
}

8、修改对话框的OnCtlColor函数。

对话框重画控件前会先用画刷重刷绘图区域。默认的画刷颜色是MFC底色灰色,如不修改,会有比较明显的闪烁情况,这里稍作处理,在自定义按钮重刷时使用空画刷。


HBRUSH CETA108_TESTDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
        HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
        // TODO: Change any attributes of the DC here
        if (pWnd == this)
        {
                return brBk;
        }
        else
        {
                int ID = pWnd->GetDlgCtrlID();
                if (( ID == IDC_BUTTON1)||(ID == IDC_BUTTON2)) 
                {
                        return (HBRUSH)GetStockObject(NULL_BRUSH);
                }
        }
        // TODO: Return a different brush if the default is not desired
        return hbr;
}

其他自定义控件的实现方法与按钮类似,根据需要,可以实现自己的ComboBox,ScrollBar,Edit等。

(4)使用GDI画波形曲线图

实际应用可以根据程序需求,使用GDI函数进行曲线界面绘制,为了防止界面闪烁,可以先把去曲线图绘制在内存DC中,最后在调用BitBlt绘制到屏幕上。

1、.对话框初始化函数中建立内存位图。

// 图片位置,大小
#define CURVE_BMP_X 42
#define CURVE_BMP_Y 72
#define CURVE_BMP_W 520
#define CURVE_BMP_H 320 // 340减去scrollbar高度
CDC m_dcMem;
CBitmap m_bitmap
BOOL CETA108_TESTDlg::OnInitDialog()
{
        …
        CDC* pDC = GetDC();
        m_dcMem.CreateCompatibleDC(pDC);
        m_bitmap.CreateCompatibleBitmap(pDC,CURVE_BMP_W,CURVE_BMP_H);
        m_dcMem.SelectObject(&m_bitmap);
        …
        ReleaseDC(pDC);
        …
}

2、在画波形图函数DrawCurve中在内存DC即 m_dcMem中画图,最后BitBlt到屏幕。

void CETA108_TESTDlg::DrawCurve()
{
        int i, ii;
        …
        CBrush brush;
        …
        brush.CreateSolidBrush(COLOR_BK);
        m_dcMem.FillRect(m_rect,&brush);
        brush.DeleteObject();
        …
        // m_dcMem中绘制坐标轴,坐标线,坐标值
        …
        // 从pBuf内读出AD数据,在m_dcMem中绘制成曲线
        …
        // m_dcMem中绘制一个图例框
        …
        CDC *pDC=GetDC();
        pDC->BitBlt(CURVE_BMP_X, CURVE_BMP_Y, CURVE_BMP_W, CURVE_BMP_H, &m_dcMem, 0, 0, SRCCOPY);
        ReleaseDC(pDC);
        return;
}

3、重写对话框OnPaint函数。

void CETA108_TESTDlg::OnPaint()
{
        CPaintDC dc(this); // device context for painting
        // TODO: Add your message handler code here
        dc.BitBlt(CURVE_BMP_X, CURVE_BMP_Y, CURVE_BMP_W, CURVE_BMP_H, &m_dcMem, 0, 0, SRCCOPY);
        // Do not call CDialog::OnPaint() for painting messages
}

4、注意事项

GDI对象应当做到Create和Delete一一对应,Get和Release一一对应,避免内存泄漏。

当SelectObject时最好记录函数返回的原对象值,然后在绘图完毕后还原。

在DrawText绘制文字前应当先设置文字背景为透明,即SetBkMode(TRANSPARENT) 。

(5)浮动信息框实现

当鼠标在曲线图上移动到坐标点附件时,显示一个浮动的信息框。

可以再建一个内存DC,设置它的位图大小为浮动信息框所占大小。当需要画信息框时,先将屏幕DC的位图BitBlt到信息框DC内,并记录相应的坐标点。

当信息框移动或改变后,先将信息框DC的备份内容还原到屏幕DC中,备份新的位置的位图,然后记录新坐标点,再在新的位置绘制信息框。

为了避免快速度移动的重画导致屏幕闪烁,可以先在一个另外的内存DC中画好,再BitBlt到屏幕上。

详细完整的代码请参考英创产品光盘中的ETA108波形采集显示例程。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分