技术博客

22/11/2022 作者 Dan Arvidson

用Excel Visual Basic为应用程序添加Kvaser CANlib

我们多数人的Windows电脑上都有Microsoft Office,我们用Microsoft Excel列表和计算,但你还可以:

  1. 从你的Excel工作表中的任意单元格向CAN总线发送数据
  2. Excel工作表的任意单元格从CAN总线接收数据

Kvaser CANlib可用于Visual Basic for Applications (VBA)。此多功能库支持Kvaser的所有适配器,并辅助你编写高级和创新的解决方案。

有这么多其他语言可以用,为什么还要使用VBA呢?首先,如果你用Microsoft Office,那么已经可使用它,而且用起来相对容易。Excel使用广泛,通过VBA,你可应用Excel的所有功能,以及你自己的创意和创新想法。如果你已经在用Excel,就没有其他成本。数据易于在不同公司之间共享;许多公司都使用Microsoft文件格式。

此博客提供什么帮助?

要按本博客中的步骤,与Kvaser CANlib一起使用Excel VBA,你需要有Microsoft Office。哪个版本的Microsoft Office都可以,但最好是Office 2010,因为VBA 7是在2010年推出的。64位和32Office均可使用(请参阅第2.23264Microsoft Office)。写此博客时,我使用了Office 365Excel(版本2202 Build 16.0.14931.2011664位。

你还需要安装用于WindowsKvaser驱动程序。请在此处查找可用的最新版本: https://www.kvaser.cn/download/ 并依照其分步安装指南。

不需要其他任何东西,但如果你需要更进一步使用CANlib,建议下载Kvaser CANlib SDK,也可以从上述同一链接获得。


介绍

Visual Basic for ApplicationsVBA)是Microsoft用于扩展Office应用程序的事件驱动编程语言。使用VBA,你可以通过自动化、Windows API和自定义功能扩展流程。你还可以操作主机应用程序的用户界面功能。

VBAMicrosoft编程语言Visual Basic 6的一个子集,它使用同一编辑器的精简版本以及类似的调试功能。因此,如果你了解VB6,也就可了解VBA的使用方法。Visual Basic是为了简化编程而创建的,这意味着用VBA编程并不难,它使用类似英语的语句来告诉电脑该做什么。

Application.ActiveDocument.SaveAs ("New Document Name.docx")

将活动文档另存为 “New Document Name.docx”

值得一提的是,VBA是单线程的,这意味着它将一次执行一个任务。请阅读第2.3多个任务处理以了解更多信息。

默认情况下,Office不显示开发人员选项卡,你必须启用它:

  • 文件选项卡中,选择选项以打开选项对话框。
  • 选择对话框左侧的自定义功能区
  • 在对话框左侧的从中选择命令(Choose Commands From)”中,选择常用命令(Common Commands)”
  • 在右侧的自定义功能区(Customize the ribbon)”中,从下拉列表中选择Main Tab,然后选择开发人员(Developer)复选框。
  • 选择OK

开发人员选项卡,你可以打开编辑器并创建按钮、下拉菜单等。还有一个快捷方式,从Office应用程序中的任何位置打开此编辑器:ALT+F11

VBA社区非常大。在网上搜索一下,几乎总能找到一个VBA示例,该示例即使和你需要的不完全相同,也至少与你需要的操作类似。


VBA范例

第一个示例将显示当某个单元出现更改时如何作出反应,并将该值发送到CAN总线。

VBA编辑器中,双击你需要对其作出反应的单元格所在的工作表,然后选择该工作表的更改(Change)”步骤。

每当该工作表中发生更改时,都会执行工作表更改事件,将其缩小到特定的一个单元格,我使用VBA中的Intersect()函数。此函数确定更改的单元格是否与我们指定的单元格匹配。为了简化此示例,我假设工作表处于活动状态,并且我们要发送的值介于0-255之间。有关canWrite中使用的参数,以及如何从CAN总线接收数据的更多信息,请见下一个示例。

在我的另一个示例中,我已将一个记录文件导入Excel(在此示例中,它是一个.ASC文件)。我这样做只是为了向CAN总线发送数据。

当然,你的数据可以是来自任何地方的,在VBA中打开一个文件,也可以在Excel中编写几个具体应发送的帧等。

我不会在这里深谈CANlib。这个示例只是给你一个关于使用VBACANlib的提示。

我将演示如何使用Kvaser CANlibExcel中的任意单元格发送数据,以及如何以Excel中的任意单元格接收数据。


编码

CANlib API- 和句柄声明

Option Explicit 'Force explicit variable declaration so that an undeclared variable generates error.
 
 
#If VBA7 Then
  Private Declare PtrSafe Sub canInitializeLibrary Lib "CANLIB32.DLL" ()
  Private Declare PtrSafe Function canUnloadLibrary Lib "CANLIB32.DLL" () As Long
  Private Declare PtrSafe Function canGetNumberOfChannels Lib "CANLIB32.DLL" (ByRef channelCount As Long) As Long
  Private Declare PtrSafe Function canGetChannelData Lib "CANLIB32.DLL" (ByVal channel As Long, ByVal item As Long, ByRef buffer As Any, ByVal bufsize As Long) As Long
  Private Declare PtrSafe Function canOpenChannel Lib "CANLIB32.DLL" (ByVal handle As Long, ByVal Flags As Long) As LongPtr
  Private Declare PtrSafe Function canClose Lib "CANLIB32.DLL" (ByVal handle As LongPtr) As Long
  Private Declare PtrSafe Function canBusOn Lib "CANLIB32.DLL" (ByVal handle As LongPtr) As Long
  Private Declare PtrSafe Function canBusOff Lib "CANLIB32.DLL" (ByVal handle As LongPtr) As Long
  Private Declare PtrSafe Function canSetBusParams Lib "CANLIB32.DLL" (ByVal handle As LongPtr, ByVal freq As Long, ByVal tseg1 As Long, ByVal tseg2 As Long, ByVal sjw As Long, ByVal noSamp As Long, ByVal syncMode As Long) As Long
  Private Declare PtrSafe Function canWrite Lib "CANLIB32.DLL" (ByVal handle As LongPtr, ByVal id As Long, ByRef msg As Any, ByVal dlc As Long, ByVal flag As Long) As Long
  Private Declare PtrSafe Function canReadWait Lib "CANLIB32.DLL" (ByVal handle As LongPtr, ByRef id As Long, ByRef msg As Any, ByRef dlc As Long, ByRef flag As Long, ByRef time As Long, ByRef timeout As Long) As Long
#Else
  Private Declare Sub canInitializeLibrary Lib "CANLIB32.DLL" ()
  Private Declare Function canUnloadLibrary Lib "CANLIB32.DLL" () As Long
  Private Declare Function canGetNumberOfChannels Lib "CANLIB32.DLL" (ByRef channelCount As Long) As Long
  Private Declare Function canGetChannelData Lib "CANLIB32.DLL" (ByVal channel As Long, ByVal item As Long, ByRef buffer As Any, ByVal bufsize As Long) As Long
  Private Declare Function canOpenChannel Lib "CANLIB32.DLL" (ByVal handle As Long, ByVal Flags As Long) As Long
  Private Declare Function canClose Lib "CANLIB32.DLL" (ByVal handle As Long) As Long
  Private Declare Function canBusOn Lib "CANLIB32.DLL" (ByVal handle As Long) As Long
  Private Declare Function canBusOff Lib "CANLIB32.DLL" (ByVal handle As Long) As Long
  Private Declare Function canSetBusParams Lib "CANLIB32.DLL" (ByVal handle As Long, ByVal freq As Long, ByVal tseg1 As Long, ByVal tseg2 As Long, ByVal sjw As Long, ByVal noSamp As Long, ByVal syncMode As Long) As Long
  Private Declare Function canWrite Lib "CANLIB32.DLL" (ByVal handle As Long, ByVal id As Long, ByRef msg As Any, ByVal dlc As Long, ByVal flag As Long) As Long
  Private Declare Function canReadWait Lib "CANLIB32.DLL" (ByVal handle As Long, ByRef id As Long, ByRef msg As Any, ByRef dlc As Long, ByRef flag As Long, ByRef time As Long, ByRef timeout As Long) As Long
#End If
 
'Constant declarations
Private Const canOK = 0
Private Const canOPEN_ACCEPT_VIRTUAL = &H20
Private Const canBITRATE_250K = -3
Private Const canCHANNELDATA_CARD_SERIAL_NO = 7
 
 
'Declaration of CAN handles
#If VBA7 Then
  Private hnd0, hnd1 As LongPtr
#Else
  Private hnd0, hnd1 As Long
#End If

这些声明是必需的,以便指定哪些dll调用可用,并指出此dll的位置。通过Kvaser的安装程序安装时,CANlib32.dll位于系统路径中。这就是说你不必指定它的具体位置。

[ Public | Private ] Declare Sub name Lib “libname” [ ( [ arglist ] ) ]

[ Public | Private ] Declare Function name Lib “libname” [ ( [ arglist ] ) ] [ As type ]

使用Private表明,只有在声明它的模块中才能访问它。

Kvaser CANlib SDK目前不包含任何VBVBA声明,因此你必须根据需要编写这些声明。有关在线Kvaser CANlib SDK,请访问:https://www.kvaser.com/canlib-webhelp/index.html

你当然可以联系我或我们的技术支持人员,我们将尽力帮助你。本文结尾处提供了联系方式。


调用 CANlib API

初始化CANlib并获取可用通道数

Sub CANLib_Start()
  Dim chCount, stat, i As Long
  Dim buffer As String
  Dim myArr(32) As Byte
  Dim ws As Worksheet
	
  canInitializeLibrary
  stat = canGetNumberOfChannels(chCount)
  If stat <> canOK Then GoTo ErrorHandler

在使用任何其他函数之前,必须先调用canInitializeLibrary函数。它将初始化驱动程序。

canGetNumberOfChannels, 此函数将返回电脑中可用CAN通道的数量。虚拟通道包括在此通道数中。

准备工作表以读取一些设备信息

Sheets.Add(Before:=Sheets(1)).name = "Device info" ' Add a sheet called "Device info" to the first position
 
Range("A1").Value = "Nof channels"
Range("B1").Value = chCount

在这里,我们给第一个位置添加一个新的工作表,并在该工作表的单元格B1中写入可用通道数。

读取每个可用通道的一些设备信息

For i = 0 To chCount - 1
  stat = canGetChannelData(i, canCHANNELDATA_CARD_SERIAL_NO, myArr(0), 32)
  buffer = StrConv(myArr(), vbUnicode)
  If buffer <> Empty Then
    Cells(i + 2, 1).Value = "Serial"
    Cells(i + 2, 2).Value = buffer
  End If
Next i

在这里,我们查看每个可用的通道,并询问设备的序列号,并且我们将其写入每个通道的第二列中的新行(即B

打开通道,设置参数并启动总线

hnd0 = canOpenChannel(0, canOPEN_ACCEPT_VIRTUAL)
hnd1 = canOpenChannel(1, canOPEN_ACCEPT_VIRTUAL)
stat = canSetBusParams(hnd0, canBITRATE_250K, 0, 0, 0, 0, 0)
If stat <> canOK Then GoTo ErrorHandler
stat = canSetBusParams(hnd1, canBITRATE_250K, 0, 0, 0, 0, 0)
If stat <> canOK Then GoTo ErrorHandler
	
stat = canBusOn(hnd0)
If stat <> canOK Then GoTo ErrorHandler
stat = canBusOn(hnd1)
If stat <> canOK Then GoTo ErrorHandler

在这里,我们打开第一个和第二个通道,以获得所有其他调用所需的句柄。我们继续准备这两个打开的通道,为它们设置相同的比特率。

为读取数据准备工作表

DeleteSheet ("Read data")
Set ws = Sheets.Add()
ws.name = "Read data"
ws.Cells(1, 1).Value = "ID"
ws.Cells(1, 2).Value = "Data1"
ws.Cells(1, 3).Value = "Data2"
ws.Cells(1, 4).Value = "Data3"
ws.Cells(1, 5).Value = "Data4"
ws.Cells(1, 6).Value = "Data5"
ws.Cells(1, 7).Value = "Data6"
ws.Cells(1, 8).Value = "Data7"
ws.Cells(1, 9).Value = "Data8"

这里我们准备一个工作表来存储读取的数据。我首先删除读取数据(Read data)”表,如果它已经存在。然后我创建此工作表并命名,同时添加一些标题注释,以便更好地理解输出内容。

发送、接收和填充单元格

Sub CANlib_Traffic()
  Dim tb As ListObject
  Dim iCol, iRow As Integer
  Dim sData(1 To 8), sCol As String
  Dim bDataTx(1 To 8) As Byte
  Dim bDataRx(1 To 8) As Byte
  Dim stat, lID, lDlc, lFlags, lTime As Long
	
	
  Worksheets("Imported ASC").Activate
  Set tb = ActiveSheet.ListObjects("TestLog")
 
  For iRow = 1 To tb.Range.Rows.Count
    lDlc = tb.DataBodyRange.Cells(iRow, tb.ListColumns("DLC").Index) ' Get how many data bytes
    For iCol = 1 To lDlc
      sCol = "Data" + Trim(Str(iCol)) 'Create the headline to read from
      sData(iCol) = tb.DataBodyRange.Cells(iRow, tb.ListColumns(sCol).Index)
      bDataTx(iCol) = CByte("&H" & sData(iCol)) ' Convert the Hex value to decimal
    Next iCol
    ' Send the byte stream of CAN data on the first channel   	
    stat = canWrite(hnd0, CLng(iRow), bDataTx(1), lDlc, 0)
    DoEvents
    ' Read out the received data on the second channel
    stat = canReadWait(hnd1, lID, bDataRx(1), lDlc, lFlags, lTime, 50)
    If stat = canOK Then
      With Worksheets("Read data") ' Populate cells in Excel with read CAN data
        .Cells(lID + 1, 1).Value = lID
        ' .Cells(lID + 1, 2).Value = CStr(Hex(bDataRx(1))) ' Use this if value should be in hexadecimal
        .Cells(lID + 1, 2).Value = bDataRx(1)
        .Cells(lID + 1, 3).Value = bDataRx(2)
        .Cells(lID + 1, 4).Value = bDataRx(3)
        .Cells(lID + 1, 5).Value = bDataRx(4)
        .Cells(lID + 1, 6).Value = bDataRx(5)
        .Cells(lID + 1, 7).Value = bDataRx(6)
        .Cells(lID + 1, 8).Value = bDataRx(7)
        .Cells(lID + 1, 9).Value = bDataRx(8)
      End With
    End If
  Next iRow
	
  MsgBox "Traffic done!"
End Sub

首先确定要读取的工作表已激活。在我的示例中,我将导入数据的工作表命名为TestLog,并将其设置为ListObject变量。我这样做是为了在获取要发送的值时更容易循环操作,这样我可以使用工作表的标题来指定我正在读取的列。第一个循环设置为读取导入数据的整个范围。我读取数据长度代码(dlc)值,以了解还要读取和稍后发送的字节数。第二个循环迭代数据字节,将它们从文本格式的十六进制转换为十进制值,并将值存储在字节数组中。

然后,通过调用CANlib函数canWrite,将CAN数据的字节数组与报文id(在本例中为行号)和数据长度代码(dlc)一起发送。

DoEvents可以更轻松地停止正在运行的宏。DoEvents函数允许中断执行代码,并允许计算机处理器同时运行其他任务。使用DoEvents会延长执行时间,但另一方面,它也会让宏停止运行。

canReadWait从接收缓冲区读取报文。如果没有可用的报文,则该函数将等待报文到达或超时。

最后,我用该读取值填充之前创建的读取数据工作表。我使用报文id指定单元格行。通过在写入单元格时使用命名工作表,我不必激活该工作表(Worksheets(“Read data”).Cells(Row, Column).Value),同时能保持保存导入数据的工作表处于活动状态。

操作后清理

Sub CANLib_Stop()
  canBusOff (hnd0)
  canBusOff (hnd1)
  canUnloadLibrary
  MsgBox "CANlib is unloaded!"
End Sub

canBusOff把指定句柄关闭。如果同一通道上没有其他句柄处于活动状态,则也将把此通道关闭。canUnloadLibrary将释放分配的内存,卸载已加载的DLL canlib32.,并取消初始化数据结构。


结果

本示例的结果如下所示:

我本可以选择将输出数据格式化为十六进制以便于比较,然后将单元格格式化为文本: ws.Columns(“A:I”).NumberFormat = “@”,当将值放入这些单元格时,需要进行如下转换:Cells(col, row).Value = CStr(Hex(MyDecValue))

但我选择了十进制,只是为了简化进一步的分析。也可以使用十六进制值,但有时需要进行转换,因为VBAExcel中的图表对象需要其值为十进制格式。

可使用Excel图表可视化数据,例如:

图表可以在VBA代码中生成,也可以在以后使用Excel的工具栏生成。要进一步深入了解VBA的功能,请参阅Microsoft文档: https://docs.microsoft.com/en-us/office/vba/api/overview/.


32位与64位Microsoft Office

两个Office版本都可用,但值得一提的是,在VBA版本7中添加了一些新的64位功能。

VBA中除数据类型Byte外,没有无符号数据类型。但这并不是完全不可行可以在VBA中读取无符号值,例如本例介绍了如何对2147483647以上的值使用双精度类型。

Private Const MAX_UINT32 = 4294967296#
Private Const MAX_INT32 = 2147483647
Function LongToUnsigned(ByVal Value As Long) As Double
  If Value < 0 Then
    LongToUnsigned = Value + MAX_UINT32
  Else
    LongToUnsigned = Value
  End If
End Function
 
Function UnsignedToLong(ByVal Value As Double) As Long
  If Value < 0 Or Value >= MAX_UINT32 Then Error 6
  If Value <= MAX_INT32 Then
    UnsignedToLong = Value
  Else
    UnsignedToLong = Value - MAX_UINT32
  End If
End Function

多任务处理

VBA是单线程的,这意味着它将一次执行一个任务。仍然可以创建一个线程或使用回调函数,如CANlib中的kvSetNotifyCallback,但我不建议这样做。如果关闭一个线程并在主线程中等待,则创建一个线程是可以的,但创建更多线程或尝试写入主线程外的单元格,可能会导致Excel冻结并关闭。为了避免麻烦和花费额外时间,建议保持它的简单性和主线程。

对于那些有使用VBA经验的用户,有一些方法可以解决这个问题,例如从代码中的循环中启动一个新的Excel实例,并从该新实例调用主工作簿中的一个过程。

我的建议是保持在主线程里。


总结

在本博客中,我们简要介绍了如何在Microsoft Excel中使用CANlib和VBA向CAN总线发送和从CAN总线接收数据。

我希望此示例可让你了解什么是可行的,尽管它并没有深入讨论Kvaser CANlib、VBA或Microsoft Excel。

你需要更多信息或是有任何问题?
请随时与我们联系。欢迎提出意见和问题!
你可以在这里找到我们:
support@kvaser.com

或者,如果你可以直接与我联系:
Dan Arvidson
用户软件经理
daar@kvaser.com

Author Image

Dan Arvidson