feat: add CAN board test tool and timing safeguards

This commit is contained in:
shenlefengnai-rgb
2026-10-01 17:43:28 +08:00
parent be4f167b83
commit 34627fa6a0
14 changed files with 607 additions and 44 deletions
+57 -20
View File
@@ -62,8 +62,10 @@ typedef struct
#define GPIO_INPUT_MASK 0x7FFFU
#define UART_LINE_SIZE 32U
#define REPORT_PERIOD_MS 500U
#define CAN_HEARTBEAT_PERIOD_MS 1000U
#define ADC_REPORT_PERIOD_MS 500U
#define PWM_REPORT_PERIOD_MS 20U
#define CAN_HEARTBEAT_PERIOD_MS 100U
#define DAC_CAN_TIMEOUT_MS 100U
#define PWM_TIMEOUT_MS 1000U
#define CAN_HEARTBEAT_STD_ID 0x6A0U
@@ -109,9 +111,12 @@ static uint16_t gpio_last_state;
static volatile uint16_t gpio_shadow_state;
static volatile uint16_t gpio_report_state;
static volatile uint16_t gpio_pending_changed_mask;
static uint32_t last_report_ms;
static uint32_t last_heartbeat_ms;
static uint32_t last_can_heartbeat_ms;
static uint32_t last_adc_report_ms;
static uint32_t last_pwm_report_ms;
static uint32_t last_heartbeat_ms;
static uint32_t last_can_heartbeat_ms;
static uint32_t dac_can_last_command_ms[2];
static bool dac_can_watchdog_active[2];
static uint16_t can_heartbeat_sequence;
static uint32_t adc_filter_history[ADC_FILTER_WINDOW];
static uint32_t adc_filter_sum;
@@ -144,6 +149,7 @@ static void PollGpioChanges(void);
static void SendPeriodicReport(void);
static void ServiceHeartbeat(void);
static void ServiceAssiLamp(void);
static void ServiceDacCanWatchdog(void);
static void SendCanHeartbeat(void);
static void HandleAssiLampSetRequest(const char *source, uint8_t state);
static void HandleTextCommand(const char *source, const char *line);
@@ -234,6 +240,7 @@ int main(void)
PollUart();
PollCanBus(&hcan1, "CAN1");
PollCanBus(&hcan2, "CAN2");
ServiceDacCanWatchdog();
ServiceAssiLamp();
PollCaptureChannels();
PollGpioChanges();
@@ -758,9 +765,14 @@ static void AppInit(void)
gpio_shadow_state = gpio_last_state;
gpio_report_state = gpio_last_state;
gpio_pending_changed_mask = 0U;
last_report_ms = HAL_GetTick();
last_heartbeat_ms = last_report_ms;
last_can_heartbeat_ms = last_report_ms;
last_adc_report_ms = HAL_GetTick();
last_pwm_report_ms = last_adc_report_ms;
last_heartbeat_ms = last_adc_report_ms;
last_can_heartbeat_ms = last_adc_report_ms;
dac_can_last_command_ms[0] = 0U;
dac_can_last_command_ms[1] = 0U;
dac_can_watchdog_active[0] = false;
dac_can_watchdog_active[1] = false;
can_heartbeat_sequence = 0U;
adc_filter_sum = 0U;
adc_filter_count = 0U;
@@ -826,10 +838,12 @@ static void PollCanBus(CAN_HandleTypeDef *hcan, const char *name)
if (rx_header.StdId == CAN_DAC_CMD_STD_ID)
{
if ((rx_header.DLC >= 3U) && (rx_data[0] >= 1U) && (rx_data[0] <= 2U))
{
uint16_t millivolts = (uint16_t)rx_data[1] | ((uint16_t)rx_data[2] << 8);
HandleDacSetRequest(name, rx_data[0], millivolts);
if ((rx_header.DLC >= 3U) && (rx_data[0] >= 1U) && (rx_data[0] <= 2U))
{
uint16_t millivolts = (uint16_t)rx_data[1] | ((uint16_t)rx_data[2] << 8);
HandleDacSetRequest(name, rx_data[0], millivolts);
dac_can_last_command_ms[rx_data[0] - 1U] = HAL_GetTick();
dac_can_watchdog_active[rx_data[0] - 1U] = true;
}
else
{
@@ -918,12 +932,12 @@ static void SendPeriodicReport(void)
{
uint32_t now = HAL_GetTick();
if ((now - last_report_ms) >= REPORT_PERIOD_MS)
{
if ((now - last_adc_report_ms) >= ADC_REPORT_PERIOD_MS)
{
uint32_t adc_mv = ReadAdcInputMillivolts();
char line[160];
last_report_ms = now;
last_adc_report_ms = now;
snprintf(line,
sizeof(line),
@@ -933,11 +947,16 @@ static void SendPeriodicReport(void)
(unsigned long)capture_channels[1].frequency_hz,
(unsigned long)capture_channels[2].frequency_hz,
(unsigned long)capture_channels[3].frequency_hz);
UartWrite(line);
BroadcastAdcReport(adc_mv);
BroadcastPwmReport();
}
}
UartWrite(line);
BroadcastAdcReport(adc_mv);
}
if ((now - last_pwm_report_ms) >= PWM_REPORT_PERIOD_MS)
{
last_pwm_report_ms = now;
BroadcastPwmReport();
}
}
static void ServiceHeartbeat(void)
{
@@ -987,6 +1006,24 @@ static void ServiceAssiLamp(void)
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_11, blue);
}
static void ServiceDacCanWatchdog(void)
{
uint32_t now = HAL_GetTick();
uint8_t dac_id;
for (dac_id = 1U; dac_id <= 2U; dac_id++)
{
uint8_t index = dac_id - 1U;
if (dac_can_watchdog_active[index] &&
((now - dac_can_last_command_ms[index]) >= DAC_CAN_TIMEOUT_MS))
{
(void)DacSetMillivolts(dac_id, 0U);
dac_can_watchdog_active[index] = false;
}
}
}
static void HandleAssiLampSetRequest(const char *source, uint8_t state)
{
char reply[64];
+17
View File
@@ -0,0 +1,17 @@
# SDC-ACQ_DAC 板卡测试上位机
发布程序位于 `output/pc-test/SDC-ACQ_DAC-TestTool.exe`,目标为 Windows x86 和 ZLG USBCAN-II。
## 使用前准备
1. 将匹配的 ZLG x86 运行时复制到程序目录:`ControlCAN.dll`,以及 `kerneldlls/usbcan.dll` 和 `kerneldlls/kerneldll.ini`。
2. 将 USB-CAN 与板卡 CAN1 或 CAN2 接在同一条 500 kbit/s 总线上,确认 CANH、CANL 和公共地正确连接。
3. 关闭其他占用同一 USB-CAN 的软件后,启动程序,选择设备索引与 CAN 通道并点击“连接”。
## 测试能力
- 显示并记录 `0x6A0` 至 `0x6A6` 的实时报文;解码心跳、电压、轮速、GPIO 和 DAC 应答。
- DAC1/DAC2 一次发送,或每 50 ms 持续发送。持续发送用于满足板卡每路 100 ms 的 DAC CAN 超时要求;停止后板卡将自行归零。
- 一键发送 5 种 ASSI RGB 状态,以及标准帧手工发送。
CAN 适配器以正常模式打开,因此会为板卡的 CAN 上报帧提供 ACK。
+6
View File
@@ -0,0 +1,6 @@
<Application x:Class="SDCAcqDacTester.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources />
</Application>
+5
View File
@@ -0,0 +1,5 @@
using System.Windows;
namespace SDCAcqDacTester;
public partial class App : Application { }
@@ -0,0 +1,130 @@
using System.Runtime.InteropServices;
namespace SDCAcqDacTester.Can;
public sealed record CanFrame(uint Id, bool IsExtended, byte[] Data, DateTime Timestamp);
public sealed class ControlCanTransport : IDisposable
{
private const uint DeviceTypeUsbCan2 = 4;
private const uint StatusOk = 1;
private CancellationTokenSource? _receiveCancel;
private Task? _receiveTask;
private uint _deviceIndex;
private uint _channelIndex;
public bool IsOpen { get; private set; }
public event EventHandler<CanFrame>? FrameReceived;
public event EventHandler<string>? ReceiveError;
[StructLayout(LayoutKind.Sequential)]
private struct VciInitConfig
{
public uint AccCode;
public uint AccMask;
public uint Reserved;
public byte Filter;
public byte Timing0;
public byte Timing1;
public byte Mode;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct VciCanObj
{
public uint Id;
public uint TimeStamp;
public byte TimeFlag;
public byte SendType;
public byte RemoteFlag;
public byte ExternFlag;
public byte DataLen;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] public byte[] Data;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] public byte[] Reserved;
}
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_OpenDevice(uint deviceType, uint deviceIndex, uint reserved);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_CloseDevice(uint deviceType, uint deviceIndex);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_InitCAN(uint deviceType, uint deviceIndex, uint canIndex, ref VciInitConfig config);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_StartCAN(uint deviceType, uint deviceIndex, uint canIndex);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_ResetCAN(uint deviceType, uint deviceIndex, uint canIndex);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_Transmit(uint deviceType, uint deviceIndex, uint canIndex, [In] VciCanObj[] send, uint length);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_Receive(uint deviceType, uint deviceIndex, uint canIndex, [Out] VciCanObj[] receive, uint length, int waitTime);
public void Open(uint deviceIndex, uint channelIndex)
{
if (IsOpen) return;
_deviceIndex = deviceIndex;
_channelIndex = channelIndex;
if (VCI_OpenDevice(DeviceTypeUsbCan2, deviceIndex, 0) != StatusOk)
throw new InvalidOperationException("打开 USBCAN-II 失败。请确认设备未被其他程序占用,并检查 ControlCAN.dll。 ");
// SJA1000 timing for 500 kbit/s, normal mode so the adapter ACKs board frames.
var config = new VciInitConfig { AccCode = 0, AccMask = 0xFFFFFFFF, Filter = 1, Timing0 = 0x00, Timing1 = 0x1C, Mode = 0 };
if (VCI_InitCAN(DeviceTypeUsbCan2, deviceIndex, channelIndex, ref config) != StatusOk)
{
VCI_CloseDevice(DeviceTypeUsbCan2, deviceIndex);
throw new InvalidOperationException("初始化 CAN 失败(500 kbit/s)。");
}
if (VCI_StartCAN(DeviceTypeUsbCan2, deviceIndex, channelIndex) != StatusOk)
{
VCI_ResetCAN(DeviceTypeUsbCan2, deviceIndex, channelIndex);
VCI_CloseDevice(DeviceTypeUsbCan2, deviceIndex);
throw new InvalidOperationException("启动 CAN 通道失败。");
}
IsOpen = true;
_receiveCancel = new CancellationTokenSource();
_receiveTask = Task.Run(() => ReceiveLoop(_receiveCancel.Token));
}
public void SendStandard(uint id, ReadOnlySpan<byte> data)
{
if (!IsOpen) throw new InvalidOperationException("CAN 未连接。");
if (id > 0x7FF || data.Length > 8) throw new ArgumentOutOfRangeException(nameof(id));
var payload = new byte[8];
data.CopyTo(payload);
var frame = new VciCanObj { Id = id, SendType = 0, RemoteFlag = 0, ExternFlag = 0, DataLen = (byte)data.Length, Data = payload, Reserved = new byte[3] };
if (VCI_Transmit(DeviceTypeUsbCan2, _deviceIndex, _channelIndex, [frame], 1) != StatusOk)
throw new InvalidOperationException("CAN 发送失败。");
}
private void ReceiveLoop(CancellationToken token)
{
var buffer = Enumerable.Range(0, 100).Select(_ => new VciCanObj { Data = new byte[8], Reserved = new byte[3] }).ToArray();
try
{
while (!token.IsCancellationRequested)
{
uint count = VCI_Receive(DeviceTypeUsbCan2, _deviceIndex, _channelIndex, buffer, (uint)buffer.Length, 50);
if (count == 0 || count == uint.MaxValue) continue;
for (var i = 0; i < Math.Min(count, (uint)buffer.Length); i++)
{
var item = buffer[i];
if (item.RemoteFlag != 0) continue;
FrameReceived?.Invoke(this, new CanFrame(item.Id, item.ExternFlag != 0, item.Data.Take(Math.Min(item.DataLen, (byte)8)).ToArray(), DateTime.Now));
}
}
}
catch (Exception ex) when (!token.IsCancellationRequested) { ReceiveError?.Invoke(this, ex.Message); }
}
public void Close()
{
if (!IsOpen) return;
_receiveCancel?.Cancel();
_receiveTask?.Wait(300);
VCI_ResetCAN(DeviceTypeUsbCan2, _deviceIndex, _channelIndex);
VCI_CloseDevice(DeviceTypeUsbCan2, _deviceIndex);
_receiveCancel?.Dispose();
_receiveCancel = null;
IsOpen = false;
}
public void Dispose() => Close();
}
+60
View File
@@ -0,0 +1,60 @@
<Window x:Class="SDCAcqDacTester.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SDC-ACQ_DAC 板卡测试工具" Height="820" Width="1230" MinHeight="680" MinWidth="1000"
Closing="Window_Closing" Background="#F5F7FA">
<Window.Resources>
<Style TargetType="GroupBox"><Setter Property="Margin" Value="8"/><Setter Property="Padding" Value="10"/><Setter Property="BorderBrush" Value="#C8D2DC"/></Style>
<Style TargetType="Button"><Setter Property="Margin" Value="3"/><Setter Property="Padding" Value="10,5"/></Style>
<Style TargetType="TextBox"><Setter Property="Margin" Value="3"/><Setter Property="Padding" Value="5"/></Style>
</Window.Resources>
<DockPanel Margin="12">
<Border DockPanel.Dock="Top" Background="#12324A" Padding="14" CornerRadius="5">
<Grid><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<StackPanel><TextBlock Foreground="White" FontSize="22" FontWeight="SemiBold" Text="SDC-ACQ_DAC 板卡测试工具"/>
<TextBlock Foreground="#C9D9E8" Margin="0,4,0,0" Text="CAN 500 kbit/s · 标准帧 · 协议 0x6A0 - 0x6A6"/></StackPanel>
<TextBlock x:Name="ConnectionText" Grid.Column="1" VerticalAlignment="Center" Foreground="#FFCC66" FontSize="15" Text="未连接"/>
</Grid>
</Border>
<Grid Margin="0,8,0,0"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<GroupBox Header="连接 - ZLG USBCAN-II(x86 ControlCAN.dll)">
<StackPanel Orientation="Horizontal"><TextBlock VerticalAlignment="Center" Text="设备索引"/><TextBox x:Name="DeviceIndexBox" Width="45" Text="0"/>
<TextBlock VerticalAlignment="Center" Margin="10,0,0,0" Text="通道"/><ComboBox x:Name="ChannelBox" Width="80" SelectedIndex="0"><ComboBoxItem Content="CAN1"/><ComboBoxItem Content="CAN2"/></ComboBox>
<Button x:Name="ConnectButton" Click="ConnectButton_Click" Content="连接" Background="#2E7D32" Foreground="White"/><Button x:Name="DisconnectButton" Click="DisconnectButton_Click" Content="断开" IsEnabled="False"/>
<TextBlock Margin="16,0,0,0" VerticalAlignment="Center" Foreground="#556575" Text="使用正常模式,适配器会 ACK 板卡上报帧。"/>
</StackPanel>
</GroupBox>
<Grid Grid.Row="1"><Grid.ColumnDefinitions><ColumnDefinition Width="2*"/><ColumnDefinition Width="1.45*"/><ColumnDefinition Width="1.2*"/></Grid.ColumnDefinitions>
<GroupBox Header="DAC 控制 - 0x6A3"><Grid><Grid.RowDefinitions><RowDefinition/><RowDefinition/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid Grid.Row="0"><Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="85"/><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" Text="DAC1 (V)"/><TextBox x:Name="Dac1Box" Grid.Column="1" Text="0"/><Button Grid.Column="2" Click="SendDac1Once_Click" Content="发送一次"/><Button x:Name="HoldDac1Button" Grid.Column="3" Click="HoldDac1Button_Click" Content="开始保持"/>
</Grid>
<Grid Grid.Row="1"><Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="85"/><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" Text="DAC2 (V)"/><TextBox x:Name="Dac2Box" Grid.Column="1" Text="0"/><Button Grid.Column="2" Click="SendDac2Once_Click" Content="发送一次"/><Button x:Name="HoldDac2Button" Grid.Column="3" Click="HoldDac2Button_Click" Content="开始保持"/>
</Grid>
<TextBlock Grid.Row="2" Margin="0,8,0,0" Foreground="#8A5200" TextWrapping="Wrap" Text="“保持”每 50 ms 发送一次,满足板卡 DAC 100 ms 超时要求;停止后,板卡将在最多 100 ms 内自动归零。"/>
</Grid></GroupBox>
<GroupBox Grid.Column="1" Header="ASSI RGB 状态 - 0x6A5"><WrapPanel>
<Button Click="Assi_Click" Tag="0" Content="关灯"/><Button Click="Assi_Click" Tag="1" Content="黄常亮" Background="#D8A500"/>
<Button Click="Assi_Click" Tag="2" Content="黄闪 2 Hz" Background="#D8A500"/><Button Click="Assi_Click" Tag="3" Content="蓝闪 2 Hz" Background="#2E6DB4" Foreground="White"/>
<Button Click="Assi_Click" Tag="4" Content="蓝常亮" Background="#2E6DB4" Foreground="White"/>
</WrapPanel></GroupBox>
<GroupBox Grid.Column="2" Header="手工帧发送"><StackPanel Orientation="Horizontal"><TextBox x:Name="RawIdBox" Width="72" Text="6A5" ToolTip="标准帧 ID,十六进制"/>
<TextBox x:Name="RawDataBox" Width="115" Text="00" ToolTip="字节,十六进制,以空格分隔"/><Button Click="SendRaw_Click" Content="发送"/>
</StackPanel></GroupBox>
</Grid>
<Grid Grid.Row="2"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<UniformGrid Rows="1" Columns="5" Margin="8"><Border Background="White" Padding="9" BorderBrush="#D5DDE5" BorderThickness="1"><StackPanel><TextBlock Text="心跳 0x6A0" Foreground="#52606D"/><TextBlock x:Name="HeartbeatValue" FontSize="17" Text="--"/></StackPanel></Border>
<Border Background="White" Padding="9" BorderBrush="#D5DDE5" BorderThickness="1"><StackPanel><TextBlock Text="输入电压 0x6A1" Foreground="#52606D"/><TextBlock x:Name="AdcValue" FontSize="17" Text="--"/></StackPanel></Border>
<Border Background="White" Padding="9" BorderBrush="#D5DDE5" BorderThickness="1"><StackPanel><TextBlock Text="轮速 0x6A2" Foreground="#52606D"/><TextBlock x:Name="PwmValue" FontSize="17" Text="--" TextWrapping="Wrap"/></StackPanel></Border>
<Border Background="White" Padding="9" BorderBrush="#D5DDE5" BorderThickness="1"><StackPanel><TextBlock Text="GPIO 0x6A6" Foreground="#52606D"/><TextBlock x:Name="GpioValue" FontSize="17" Text="--"/></StackPanel></Border>
<Border Background="White" Padding="9" BorderBrush="#D5DDE5" BorderThickness="1"><StackPanel><TextBlock Text="DAC 应答 0x6A4" Foreground="#52606D"/><TextBlock x:Name="DacAckValue" FontSize="17" Text="--"/></StackPanel></Border>
</UniformGrid>
<GroupBox Grid.Row="1" Header="CAN 收发记录"><DataGrid x:Name="LogGrid" IsReadOnly="True" AutoGenerateColumns="False" CanUserAddRows="False" HeadersVisibility="Column"><DataGrid.Columns>
<DataGridTextColumn Header="时间" Binding="{Binding Time}" Width="135"/><DataGridTextColumn Header="方向" Binding="{Binding Direction}" Width="65"/><DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="70"/>
<DataGridTextColumn Header="DLC" Binding="{Binding Dlc}" Width="50"/><DataGridTextColumn Header="数据" Binding="{Binding Data}" Width="180"/><DataGridTextColumn Header="解释" Binding="{Binding Description}" Width="*"/>
</DataGrid.Columns></DataGrid></GroupBox>
</Grid>
</Grid>
</DockPanel>
</Window>
+194
View File
@@ -0,0 +1,194 @@
using System.Collections.ObjectModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using SDCAcqDacTester.Can;
namespace SDCAcqDacTester;
public partial class MainWindow : Window
{
private const uint HeartbeatId = 0x6A0;
private const uint AdcId = 0x6A1;
private const uint PwmId = 0x6A2;
private const uint DacCommandId = 0x6A3;
private const uint DacAckId = 0x6A4;
private const uint AssiCommandId = 0x6A5;
private const uint GpioId = 0x6A6;
private readonly ObservableCollection<LogRow> _rows = [];
private readonly DispatcherTimer _holdTimer;
private ControlCanTransport? _can;
private bool _holdDac1;
private bool _holdDac2;
public MainWindow()
{
InitializeComponent();
LogGrid.ItemsSource = _rows;
_holdTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
_holdTimer.Tick += HoldTimer_Tick;
}
private void ConnectButton_Click(object sender, RoutedEventArgs e)
{
try
{
if (!uint.TryParse(DeviceIndexBox.Text, out uint index)) throw new InvalidOperationException("设备索引必须为非负整数。");
_can = new ControlCanTransport();
_can.FrameReceived += Can_FrameReceived;
_can.ReceiveError += Can_ReceiveError;
_can.Open(index, (uint)ChannelBox.SelectedIndex);
ConnectionText.Text = $"已连接:USBCAN-II / CAN{ChannelBox.SelectedIndex + 1}";
ConnectionText.Foreground = System.Windows.Media.Brushes.LightGreen;
ConnectButton.IsEnabled = false;
DisconnectButton.IsEnabled = true;
AddLog("SYS", "-", [], "已连接,500 kbit/s 正常模式。");
}
catch (Exception ex) { ShowError(ex.Message); }
}
private void DisconnectButton_Click(object sender, RoutedEventArgs e) => Disconnect();
private void SendDac1Once_Click(object sender, RoutedEventArgs e) => SendDac(1, ReadVolts(Dac1Box));
private void SendDac2Once_Click(object sender, RoutedEventArgs e) => SendDac(2, ReadVolts(Dac2Box));
private void HoldDac1Button_Click(object sender, RoutedEventArgs e)
{
_holdDac1 = !_holdDac1;
HoldDac1Button.Content = _holdDac1 ? "停止保持" : "开始保持";
UpdateHoldTimer();
}
private void HoldDac2Button_Click(object sender, RoutedEventArgs e)
{
_holdDac2 = !_holdDac2;
HoldDac2Button.Content = _holdDac2 ? "停止保持" : "开始保持";
UpdateHoldTimer();
}
private void HoldTimer_Tick(object? sender, EventArgs e)
{
try
{
if (_holdDac1) SendDac(1, ReadVolts(Dac1Box));
if (_holdDac2) SendDac(2, ReadVolts(Dac2Box));
}
catch (Exception ex)
{
_holdDac1 = _holdDac2 = false;
HoldDac1Button.Content = HoldDac2Button.Content = "开始保持";
UpdateHoldTimer();
ShowError(ex.Message);
}
}
private void Assi_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: string value } || !byte.TryParse(value, out byte state)) return;
Send(AssiCommandId, [state], $"ASSI 状态={state}");
}
private void SendRaw_Click(object sender, RoutedEventArgs e)
{
try
{
uint id = uint.Parse(RawIdBox.Text.Trim().Replace("0x", "", StringComparison.OrdinalIgnoreCase), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
byte[] data = string.IsNullOrWhiteSpace(RawDataBox.Text) ? [] : RawDataBox.Text.Split([' ', ',', ';'], StringSplitOptions.RemoveEmptyEntries)
.Select(x => byte.Parse(x.Replace("0x", "", StringComparison.OrdinalIgnoreCase), NumberStyles.HexNumber, CultureInfo.InvariantCulture)).ToArray();
Send(id, data, "手工帧");
}
catch (Exception ex) { ShowError($"手工帧格式错误:{ex.Message}"); }
}
private void SendDac(byte dac, ushort millivolts)
{
Send(DacCommandId, [dac, (byte)(millivolts & 0xFF), (byte)(millivolts >> 8)], $"DAC{dac}={millivolts} mV");
}
private void Send(uint id, byte[] data, string description)
{
try
{
if (_can is not { IsOpen: true }) throw new InvalidOperationException("请先连接 CAN。 ");
_can.SendStandard(id, data);
AddLog("TX", $"0x{id:X3}", data, description);
}
catch (Exception ex) { ShowError(ex.Message); }
}
private static ushort ReadVolts(TextBox box)
{
if (!decimal.TryParse(box.Text, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal volts) &&
!decimal.TryParse(box.Text, NumberStyles.Number, CultureInfo.CurrentCulture, out volts))
throw new InvalidOperationException("DAC 电压必须是 0 - 5 V 的数值。");
if (volts is < 0 or > 5) throw new InvalidOperationException("DAC 电压范围为 0 - 5 V。");
return (ushort)Math.Round(volts * 1000m, MidpointRounding.AwayFromZero);
}
private void Can_FrameReceived(object? sender, CanFrame frame) => Dispatcher.Invoke(() =>
{
string description = Decode(frame);
AddLog("RX", frame.IsExtended ? $"0x{frame.Id:X8}" : $"0x{frame.Id:X3}", frame.Data, description, frame.Timestamp);
});
private void Can_ReceiveError(object? sender, string error) => Dispatcher.Invoke(() => ShowError($"接收异常:{error}"));
private string Decode(CanFrame frame)
{
var d = frame.Data;
switch (frame.Id)
{
case HeartbeatId when d.Length >= 8:
string heartbeat = $"板号=0x{d[0]:X2} 协议={d[1]} 序号={U16(d, 2)} 运行={U32(d, 4)} ms";
HeartbeatValue.Text = heartbeat; return heartbeat;
case AdcId when d.Length >= 2:
string adc = $"{U16(d, 0)} mV ({U16(d, 0) / 1000.0:F3} V)";
AdcValue.Text = adc; return adc;
case PwmId when d.Length >= 8:
string pwm = string.Join(" / ", Enumerable.Range(0, 4).Select(i => $"{U16(d, i * 2)} Hz"));
PwmValue.Text = pwm; return pwm;
case DacAckId when d.Length >= 4:
string ack = $"DAC{d[1]} {(d[0] == 1 ? "OK" : "ERR")} = {U16(d, 2)} mV";
DacAckValue.Text = ack; return ack;
case GpioId when d.Length >= 8:
string gpio = $"状态=0x{U16(d, 0):X4} 变化=0x{U16(d, 2):X4} 时间={U32(d, 4)} ms";
GpioValue.Text = gpio; return gpio;
default: return "未定义帧";
}
}
private static ushort U16(byte[] data, int offset) => (ushort)(data[offset] | (data[offset + 1] << 8));
private static uint U32(byte[] data, int offset) => (uint)(data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24));
private void AddLog(string direction, string id, byte[] data, string description, DateTime? timestamp = null)
{
_rows.Insert(0, new LogRow((timestamp ?? DateTime.Now).ToString("HH:mm:ss.fff"), direction, id, data.Length.ToString(), BitConverter.ToString(data).Replace('-', ' '), description));
while (_rows.Count > 500) _rows.RemoveAt(_rows.Count - 1);
}
private void UpdateHoldTimer()
{
if (_holdDac1 || _holdDac2) _holdTimer.Start(); else _holdTimer.Stop();
}
private void Disconnect()
{
_holdDac1 = _holdDac2 = false;
_holdTimer.Stop();
_can?.Dispose(); _can = null;
ConnectionText.Text = "未连接"; ConnectionText.Foreground = System.Windows.Media.Brushes.Orange;
ConnectButton.IsEnabled = true; DisconnectButton.IsEnabled = false;
}
private void ShowError(string message)
{
AddLog("ERR", "-", [], message);
MessageBox.Show(message, "SDC-ACQ_DAC 测试工具", MessageBoxButton.OK, MessageBoxImage.Warning);
}
private void Window_Closing(object? sender, System.ComponentModel.CancelEventArgs e) => Disconnect();
}
public sealed record LogRow(string Time, string Direction, string Id, string Dlc, string Data, string Description);
@@ -0,0 +1,9 @@
# ZLG USBCAN-II driver files
Copy the complete matching x86 vendor runtime into this directory before using hardware CAN:
- `ControlCAN.dll` beside `SDC-ACQ_DAC-TestTool.exe`
- `kerneldlls\\usbcan.dll`
- `kerneldlls\\kerneldll.ini`
Do not run another CAN application against the same adapter at the same time.
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Platforms>x86</Platforms>
<PlatformTarget>x86</PlatformTarget>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>SDC-ACQ_DAC-TestTool</AssemblyName>
</PropertyGroup>
<ItemGroup>
<None Include="Native\\x86\\README.md" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
+4 -4
View File
@@ -77,7 +77,7 @@
<table>
<tr><th style="width:20%">功能</th><th>当前实现</th></tr>
<tr><td>状态采集</td><td>采集 PD0–PD14 共 15 路数字输入。上电和任一输入变化时,向 CAN1、CAN2 发送完整状态与变化掩码。</td></tr>
<tr><td>频率采集</td><td>采集 PA0、PA2、PA6、PB14 的上升沿频率,每 500 ms 通过 CAN 和串口输出。</td></tr>
<tr><td>频率采集</td><td>采集 PA0、PA2、PA6、PB14 的上升沿频率,每 20 ms 通过 CAN 输出;串口每 500 ms 汇总输出。</td></tr>
<tr><td>电压采集</td><td>ADC1_IN8(PB0)测量外部经分压后的输入;对外输出分压前、已标定且滤波后的电压。</td></tr>
<tr><td>CAN</td><td>CAN1 与 CAN2 均为 500 kbit/s、11 位标准数据帧,并使用相同协议。</td></tr>
<tr><td>DAC</td><td>通过 I2C2 控制两颗 MCP4725,接收 CAN 命令或串口命令设置 0–5.000 V 输出。</td></tr>
@@ -99,10 +99,10 @@
<p>所有字段均为小端序(低字节在前)。板卡主动上报的帧会分别发到 CAN1 和 CAN2。</p>
<table>
<tr><th style="width:13%">CAN ID</th><th style="width:19%">名称</th><th style="width:22%">方向 / 时机</th><th>数据定义</th></tr>
<tr><td><code>0x6A0</code></td><td>心跳</td><td>板卡发送,每 1 s</td><td><code>A1 01 seq_L seq_H uptime_0..3</code>;板号 A1、协议版本 1、序号、运行时间 ms。</td></tr>
<tr><td><code>0x6A0</code></td><td>心跳</td><td>板卡发送,每 100 ms</td><td><code>A1 01 seq_L seq_H uptime_0..3</code>;板号 A1、协议版本 1、序号、运行时间 ms。</td></tr>
<tr><td><code>0x6A1</code></td><td>输入电压</td><td>板卡发送,每 500 ms</td><td><code>uint16</code>,单位 mV。例如 <code>90 2F</code> = 0x2F90 = 12176 mV = 12.176 V。</td></tr>
<tr><td><code>0x6A2</code></td><td>四路频率</td><td>板卡发送,每 500 ms</td><td>4 个 <code>uint16</code>,单位 Hz;依次为 PA0、PA2、PA6、PB14。</td></tr>
<tr><td><code>0x6A3</code></td><td>DAC 设置</td><td>外部发送给板卡</td><td><code>[DAC 编号][mV_L][mV_H]</code>,编号仅允许 1 或 2。</td></tr>
<tr><td><code>0x6A2</code></td><td>四路频率</td><td>板卡发送,每 20 ms</td><td>4 个 <code>uint16</code>,单位 Hz;依次为 PA0、PA2、PA6、PB14。</td></tr>
<tr><td><code>0x6A3</code></td><td>DAC 设置</td><td>外部发送给板卡</td><td><code>[DAC 编号][mV_L][mV_H]</code>,编号仅允许 1 或 2;每路 100 ms 未续帧归零。</td></tr>
<tr><td><code>0x6A4</code></td><td>DAC 应答</td><td>板卡发送,设置后</td><td><code>[结果][DAC 编号][mV_L][mV_H]</code>;结果 1 成功,0 失败。</td></tr>
<tr><td><code>0x6A5</code></td><td>ASSI RGB 灯控</td><td>外部发送给板卡</td><td><code>byte0</code>:0=关、1=黄常亮、2=黄闪、3=蓝闪、4=蓝常亮。</td></tr>
<tr><td><code>0x6A6</code></td><td>15 路状态</td><td>板卡发送,上电或状态变化</td><td>状态 <code>uint16</code>、变化掩码 <code>uint16</code>、时间戳 <code>uint32 ms</code>。</td></tr>
+10 -10
View File
@@ -72,17 +72,17 @@ VAL_ 1700 DAC_Set_Result 0 "Failed" 1 "OK" ;
VAL_ 1700 DAC_Select 1 "DAC1" 2 "DAC2" ;
VAL_ 1701 ASSI_State 0 "AS_OFF" 1 "AS_READY" 2 "AS_DRIVING" 3 "AS_EMERGENCY" 4 "AS_FINISHED" ;
CM_ BO_ 1696 "Standard ID 0x6A0. Sent every 1 s on CAN1 and CAN2.";
CM_ BO_ 1696 "Standard ID 0x6A0. Sent every 100 ms on CAN1 and CAN2.";
CM_ BO_ 1697 "Standard ID 0x6A1. Filtered and calibrated physical input voltage before the hardware divider.";
CM_ BO_ 1698 "Standard ID 0x6A2. Sent every 500 ms. Frequency order is PA0, PA2, PA6, PB14.";
CM_ BO_ 1699 "Standard ID 0x6A3. Accepted on CAN1 and CAN2. The on-wire voltage is an unsigned mV value.";
CM_ BO_ 1698 "Standard ID 0x6A2. Sent every 20 ms. Frequency order is PA0, PA2, PA6, PB14.";
CM_ BO_ 1699 "Standard ID 0x6A3. Accepted on CAN1 and CAN2. The on-wire voltage is an unsigned mV value. Each DAC is reset to 0 mV if its own control frame is not received again within 100 ms.";
CM_ BO_ 1700 "Standard ID 0x6A4. Sent on CAN1 and CAN2 after a DAC command. Result 0 indicates the I2C write failed.";
CM_ BO_ 1701 "Standard ID 0x6A5. Accepted on CAN1 and CAN2. Byte 0 selects the latched ASSI RGB lamp state: AS_OFF, AS_READY, AS_DRIVING, AS_EMERGENCY, or AS_FINISHED.";
CM_ BO_ 1702 "Standard ID 0x6A6. Sent at startup and whenever PD0-PD14 state changes, on CAN1 and CAN2.";
CM_ SG_ 256 Board_Identifier "Fixed board identifier 0xA1.";
CM_ SG_ 259 Frequency_PA0 "TIM5_CH1 input capture on PA0.";
CM_ SG_ 259 Frequency_PA2 "TIM9_CH1 input capture on PA2.";
CM_ SG_ 259 Frequency_PA6 "TIM3_CH1 input capture on PA6.";
CM_ SG_ 259 Frequency_PB14 "TIM12_CH1 input capture on PB14.";
CM_ SG_ 816 SDC_Input_State "Bit 0 through bit 14 map directly to PD0 through PD14.";
CM_ SG_ 816 SDC_Changed_Mask "A set bit identifies an input that changed since the preceding GPIO-status frame.";
CM_ SG_ 1696 Board_Identifier "Fixed board identifier 0xA1.";
CM_ SG_ 1698 Frequency_PA0 "TIM5_CH1 input capture on PA0.";
CM_ SG_ 1698 Frequency_PA2 "TIM9_CH1 input capture on PA2.";
CM_ SG_ 1698 Frequency_PA6 "TIM3_CH1 input capture on PA6.";
CM_ SG_ 1698 Frequency_PB14 "TIM12_CH1 input capture on PB14.";
CM_ SG_ 1702 SDC_Input_State "Bit 0 through bit 14 map directly to PD0 through PD14.";
CM_ SG_ 1702 SDC_Changed_Mask "A set bit identifies an input that changed since the preceding GPIO-status frame.";
+88
View File
@@ -0,0 +1,88 @@
VERSION "1.1"
NS_ :
NS_DESC_
CM_
BA_DEF_
BA_
VAL_
CAT_DEF_
CAT_
FILTER
BA_DEF_DEF_
EV_DATA_
ENVVAR_DATA_
SGTYPE_
SGTYPE_VAL_
BA_DEF_SGTYPE_
BA_SGTYPE_
SIG_TYPE_REF_
VAL_TABLE_
SIG_GROUP_
SIG_VALTYPE_
SIGTYPE_VALTYPE_
BO_TX_BU_
BA_DEF_REL_
BA_REL_
BA_DEF_DEF_REL_
BU_SG_REL_
BU_EV_REL_
BU_BO_REL_
SG_MUL_VAL_
BS_:
BU_: SDC_ACQ_DAC VehicleController
BO_ 1696 SDC_Heartbeat: 8 SDC_ACQ_DAC
SG_ Board_Identifier : 0|8@1+ (1,0) [0|255] "" VehicleController
SG_ Protocol_Version : 8|8@1+ (1,0) [0|255] "" VehicleController
SG_ Heartbeat_Sequence : 16|16@1+ (1,0) [0|65535] "" VehicleController
SG_ Uptime : 32|32@1+ (0.001,0) [0|4294967.295] "s" VehicleController
BO_ 1697 SDC_ADC_InputVoltage: 2 SDC_ACQ_DAC
SG_ Input_Voltage : 0|16@1+ (0.001,0) [0|65.535] "V" VehicleController
BO_ 1698 SDC_InputFrequencies: 8 SDC_ACQ_DAC
SG_ Frequency_PA0 : 0|16@1+ (1,0) [0|65535] "Hz" VehicleController
SG_ Frequency_PA2 : 16|16@1+ (1,0) [0|65535] "Hz" VehicleController
SG_ Frequency_PA6 : 32|16@1+ (1,0) [0|65535] "Hz" VehicleController
SG_ Frequency_PB14 : 48|16@1+ (1,0) [0|65535] "Hz" VehicleController
BO_ 1699 SDC_DAC_Set: 3 VehicleController
SG_ DAC_Select : 0|8@1+ (1,0) [1|2] "" SDC_ACQ_DAC
SG_ DAC_Command_Voltage : 8|16@1+ (0.001,0) [0|5] "V" SDC_ACQ_DAC
BO_ 1700 SDC_DAC_Ack: 4 SDC_ACQ_DAC
SG_ DAC_Set_Result : 0|8@1+ (1,0) [0|1] "" VehicleController
SG_ DAC_Select : 8|8@1+ (1,0) [1|2] "" VehicleController
SG_ DAC_Output_Voltage : 16|16@1+ (0.001,0) [0|5] "V" VehicleController
BO_ 1701 ASSI_Lamp_Set: 1 VehicleController
SG_ ASSI_State : 0|8@1+ (1,0) [0|4] "" SDC_ACQ_DAC
BO_ 1702 SDC_GPIO_Status: 8 SDC_ACQ_DAC
SG_ SDC_Input_State : 0|15@1+ (1,0) [0|32767] "" VehicleController
SG_ SDC_Changed_Mask : 16|15@1+ (1,0) [0|32767] "" VehicleController
SG_ Timestamp : 32|32@1+ (0.001,0) [0|4294967.295] "s" VehicleController
VAL_ 1696 Board_Identifier 161 "A11_F407" ;
VAL_ 1699 DAC_Select 1 "DAC1" 2 "DAC2" ;
VAL_ 1700 DAC_Set_Result 0 "Failed" 1 "OK" ;
VAL_ 1700 DAC_Select 1 "DAC1" 2 "DAC2" ;
VAL_ 1701 ASSI_State 0 "AS_OFF" 1 "AS_READY" 2 "AS_DRIVING" 3 "AS_EMERGENCY" 4 "AS_FINISHED" ;
CM_ BO_ 1696 "Standard ID 0x6A0. Sent every 100 ms on CAN1 and CAN2.";
CM_ BO_ 1697 "Standard ID 0x6A1. Filtered and calibrated physical input voltage before the hardware divider.";
CM_ BO_ 1698 "Standard ID 0x6A2. Sent every 20 ms. Frequency order is PA0, PA2, PA6, PB14.";
CM_ BO_ 1699 "Standard ID 0x6A3. Accepted on CAN1 and CAN2. The on-wire voltage is an unsigned mV value. Each DAC is reset to 0 mV if its own control frame is not received again within 100 ms.";
CM_ BO_ 1700 "Standard ID 0x6A4. Sent on CAN1 and CAN2 after a DAC command. Result 0 indicates the I2C write failed.";
CM_ BO_ 1701 "Standard ID 0x6A5. Accepted on CAN1 and CAN2. Byte 0 selects the latched ASSI RGB lamp state: AS_OFF, AS_READY, AS_DRIVING, AS_EMERGENCY, or AS_FINISHED.";
CM_ BO_ 1702 "Standard ID 0x6A6. Sent at startup and whenever PD0-PD14 state changes, on CAN1 and CAN2.";
CM_ SG_ 1696 Board_Identifier "Fixed board identifier 0xA1.";
CM_ SG_ 1698 Frequency_PA0 "TIM5_CH1 input capture on PA0.";
CM_ SG_ 1698 Frequency_PA2 "TIM9_CH1 input capture on PA2.";
CM_ SG_ 1698 Frequency_PA6 "TIM3_CH1 input capture on PA6.";
CM_ SG_ 1698 Frequency_PB14 "TIM12_CH1 input capture on PB14.";
CM_ SG_ 1702 SDC_Input_State "Bit 0 through bit 14 map directly to PD0 through PD14.";
CM_ SG_ 1702 SDC_Changed_Mask "A set bit identifies an input that changed since the preceding GPIO-status frame.";
+5 -5
View File
@@ -47,7 +47,7 @@ endobj
endobj
6 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260918211423+08'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260918211423+08'00') /Producer (ReportLab PDF Library - \(opensource\))
/Author (\(anonymous\)) /CreationDate (D:20260922213520+08'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260922213520+08'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\376\377\000S\000D\000C\000-\000A\000C\000Q\000_\000D\000A\000C\000 \000C\000A\000N\000 b\245e\207\213\364f\016\210h) /Trapped /False
>>
endobj
@@ -58,10 +58,10 @@ endobj
endobj
8 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2111
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2151
>>
stream
Gb!;fh/h((&:`#5Tg!Re16%B=ojKYo"7u]ZKS7Mh@60e&L5&MI+XA`F*r+L%P0]<QZgZ0h,jhBBQ?KZ>Xn:@RY>6o>J'tgP:)LT+A(Q3m9EL\HB2W`gB%u\8fuDbU3Tj?Ff<R4ZdpT]f%5a-e>Ss\;1G4Zm_CuIBs87+oABM-8@C$VN!gkOp&tn%^JB2R.5kNa%$e3En#VgJH:TjN@2-[5o'tP(14:Nj(^qu9Wg[Sd/'e"K0JZok[XO*p/Q&p_]il1Y.p6*\'Z)ptQ1;SYCqTL'>[p#-D%/gT$"pV@H'S%)>a3k5t0Wp*^$<XNrijuBokoSYpGZ+pfTe+?"WeA%<ZX4RYEIGnR:nb:^5]@b2)A*n6s2m3SY5-6umt.p)HDXYMV0]].q_./?4/l&DKC@s^n`b/arcHar*OH.$mtHs87'dW&q-9WEF@8Zp\F7^Vk=4D(H@9al$2]OiDCss+D@ke^-h\NDAlVB.8lXVVeqTY,9$+m&^p"L_V_jH8AJRhF^lie9m/2Z&^ICQGhjd1PU;_ascGN:$V'Utp2MIi_<*IYK%("G%P!Sa1XH6\UQ%CSuaAp2O\ZgHWhJ&+u&0;(\i3G2mda[G(r(n2GkknZp[Xsts"XH$;p].^T\/B4&]M5L7$[9]pNeSNGVD@4nUm4C[WC%0?b#p#U#'J?C$OTHMGU[aCH'I6[$a>6f!\#X!Am/V9WH?^C;IE$d^6K+.;JB6VB&:pOHS65#8u=3(KVIBQXNX\EkHN7'SNa1V@HUsl_gKuW3OumcCL%9A?jUgGN7$r`K38ff_\Y@443J"?mGN[^Y,&JS5J/rTV?2;+fCqW<HPfaWUb0.CH-0U<kpDn9]Defs$=&RB1#uY/0I@EemsV&fJJXP]/A:gI1fhK,jM#D,/!K0^\&'Z#E4HpQ#M-YTRRVBMjdIFG]FEQLp/:235^aG(PEWfV1sH@3=la<5=NuHS[Yt$#9KmR3.C69S8K$!Oo=N%-pn7eV$q61;2$2b-eEnJ!VMl7s99"OL-V6r(kRQ]J'g:5GPYKiOO'YgoFHZYT*'F<*0%8FnZ+_P`];tHZH-.>-RYD;g4^Z[)Y+Kf@:Ejc1ojld"%G(^B>u.4hT+7VYO+RlA:6+2sWQ/+NV=t#7VZGQ/)jgX[2o(kGgqB?6H9]%H7D,`*RXe$6)N4i'VKQ#CZohp'$tu';rAl[I;<Ha;NA,ks^nc.?T5n-C<$3iAR:S88!XhH,aKNS`dAI)=L4JJ%qK,QScrfUY&<ks3`[53B1M5(?gL1J)(#EeIM/^LtE6hL$"kl&Jm\#$s$V$mS:KO@d:.+$-l.W6\5-$s%4VR@Va;Dj>VD6\Q2i`W:ShBuB5OR1FkGI\3M5RfACWtZ48KOp.C1-bWCdGGRcOVN,d7H1f8o3Rspp9"c<gSV<2%lOLb$@RqcI3Lb0?<(YIV#h)D<#a#MYOFjQ\ZT6C8P:gJ>sLseN@+AT_b&X.3;F;;\=^j"2:m[\tHb$5V)PaB%pZ!4</8<k2'bD\#VcNiY"PV7tNKp\LRuTH+Hm?9,Z&pL)`lG%?3#tZa2SSG.'3eQCpN'Am09c36KWfbgWD<FoU^mA&rqVl5S@W9FZm$72+;tL^62A6D4mH*WONg`;g4^fpc#ilm0QXZZO_A%;3U1gL>+S7M(5MB8]WDR(9f/n9p_4-8Q:ks.`1bk-pN%b0gtkWpYthLA*`']cMhLi_@(NNlc5P(e#6_*W`"gdffuI^ji9_re5)`k7nj*Hds`,O"NWH<T7CcEj&Yqp];Y'9(Ls3l<*[>h<N&-4$)B^`>Uc3YOqk<%;sPph.C!?J+X'<]fR/Q2>]I_2F!%jJmJ+.RsL62'1=T,>)Y&!!C?.[H]-srJY\Ij>rRifSHX"6C"a9d2$=;(!qdI`.bY$j2$?IZh^i]=p/b"CjuI<NC0g]B7h2(\#D9[Wh)n1W$ETBJaJ%)0-UU+bE.Y0k.nfgL=p0ShFMsCOdO7V$'ZoO\*PPbYJ*U/j`Va@%(fAp(f_fu(\,0XeH$PLr)amsP-I-k@IAr84-eBgDmC/q3hlb`SMEBJ9ITp0O_k]^*5Aq=/U?IF,+uJhR,DJ/BhP@6p'UfRsEZ]VX532S;J3HcE2Z<`&/6i#~>endstream
Gb!;fh/h((&:`#5Tg!QhR79`J0:Nh#kSMSP$(!`jF>-9*)FC,(+b;*3d(d7Eq=JfBT?eu4mA(J_G-tMWp:%YsNt[7tgpPI($2JI.DZ.K[#;UWl\>5;3[`OT:NOKjSc7(9DGcbbU^eb=EE#t/kdpSsqP\Y!#-5>25`Q8>,jC&p#AuE-A&74*;5hQ<+#\kghUsFKsU2DlJZ)MShYg)pNfs8lYd%/hO0X*qH#u>GTa,M)s+OtH6bV[+(,IDjG0Wq[u?@G7RYK6&WfueG('27o!b7%WQUJu*Td*9@N?pHe>"[WM^7BMuW#ZXPu'eJ$0;WTK1gIJjpc;[J8W2htJ.O/kT]IseM\CG4)BS8)L'4<oE2Cp\Fs0Z^(D"EF!h?@V[m7].M9'pZ2%q">2B$ps^%rTV1Mo4f9_s+>13[c#E56s>@OCfCSo\YX)$F`RaW7\S+Zu\=u42!i17Yg_o\o+Y2drl;^2J;aMhe$#2>&ti'[omFFCT'5o@fjP?31Sqi>a6-kfhBnoK-V8RjMah-CB*EY]OD2.cuU739?mu=XHfqF3`9f[aZ@Y/CIeZ\`A8XIeq8P`'ZcQiTNkUt:+g3gb[:ncYS=AWg/>EW_VEK^fS#6*/_DPQUD2WcUYu=)1?)p<guqJSKI(!RL'in@V,/%g8)/'cqUE?3B)&H9^k4]DPU2$nB!c0MfK><o:_f@(!s.SEOF#W:7&bRbP#@rM8@b!BQ!>e@[:_,=CqD)4PffjTkp0A""Z*ejWK,4aIt%*u!<G7m]FOO5KB17+<RJCFR_U'*<Z%SfPQnI)!r<qK:ch#pAk]BqT/>>jl#/2CJ(4NaM!Op]KftHrB$s`THC;&?_nQtef@O@g!Y^o%OE[MI;jT*]2D]p_0IX:[cjFQKPrP_:m5")Em]T`n9VqnQM,Rm*f=f<ilefsF':*hqD0'N_\"U=d?:hgPW:*'HEZ4]_oA/VAiPR3#bT`QoQf$Pe][,?;cnSQm*il)pq^::4T`\L<7?4fS!bTX,m:.-U!Z%$&PSRNL=(kCt@0tKBPPKmNQ$N5eFTYfYjj;H,,@9:X6Ot7XH4%'lT$UE5m'tEk<P93'SjM27&UIi4kjcf;kqFUX=P*V0amj^8FVs^NH69qM(9m2#JaNQF#M/P;m_di2qr(KgnMUY0#$2'e(^m];mB-\-A]eJm>iTOH9q]=;N+K<k0ihu=09b8+1B/,"kl(X)(K]2VZ-($*"oQeS\."!AY[DL0r,N2M>p]!>%tdoV:Btb5N40jh!]4#K?F)BM(t8<5GMs:t&CaEsKh*")O^>!A!72N)]`@hQ(`3_S10k'T5W+F+V<d,(iq!AMg!^PjR>bI"(E.?(G`0u=0%6YDoqr0UEb24uHKM.tC5mpo2p#M$=UWG+R0c:9dep%P&;,YN\J/@f_S8,H\'`t9.5lEkCE<)F]9k^jEO^eLX)6BM"El)p&F=E_fF*tVQE4Di.k"S^"abUg1>9I^g!"maLc9^L\6dr,g3!k[bL*gQe"m<Dkl)]"0SRXTkq<VVP'%Nb^`=?p(ue>Yo](RV_jt0$\QF8C"A6M5&<poT-ju<PQ4.#q/"T#2I-NrE#1H:6j)3Smq&>ZVe"^_'W#(JJ(0q$&Ho)hdFQm,fI?FEZp]C<%otU\a/LVs45"e?V+abtl0mQU_\8Q?n"(Vi'Dj"YSnd;u<QM/m$_)X:B</d3&@0g;8MT*Y1bZrsIHsc!+Po^8#s3Zg+i?t#Hn6/nAWpX95LA*1$iuWn"i_7"MO7D-6"LF#j&HSWZo*'Jn^jkPlrA38@>r85IY>'=W4o,el]kn6K`UKo&LX.(u]9,9\IEND$cJhhrhgo-jaicR0epbX=?YrUa%%oQ.$NFnV_scF9TL+dU_UnS3pk@CEs!1N5@OZk>_^o4i=F(,'YPALHFot<JbM-nW5s>\9XBMM9m!426P^knmOB"ogSgQf&qL3]!D7m/(puhEE>cBVN=E<Un3bUhXD#YKSJr(6LQ5hrW53_qZL)"60HVN&8TNGTHMd).J+HB?.Zpa^hg<JeRi(nf<;pBed,9dJYJ^'7bG8F&C$\nuT:CunF1NGt*G'EO7"M'4rk'ed;rZpW$?euB/op^?Y%[31rgq6d7MP;?LX3efVYR&;@cD&Na$9j0)Hk/+,*dmq\J?DF.2Z<a(l#A-~>endstream
endobj
xref
0 9
@@ -77,7 +77,7 @@ xref
trailer
<<
/ID
[<0c601b021e0ca62988154610c4965733><0c601b021e0ca62988154610c4965733>]
[<e1e46c8f2a916049ff773458531b69dd><e1e46c8f2a916049ff773458531b69dd>]
% ReportLab generated PDF document -- digest (opensource)
/Info 6 0 R
@@ -85,5 +85,5 @@ trailer
/Size 9
>>
startxref
4057
4097
%%EOF
+7 -5
View File
@@ -12,8 +12,8 @@
2. 接收 CAN1、CAN2 的 DAC 控制帧。
3. 读取四路定时器捕获值并计算频率。
4. 汇总 15 路 GPIO 变化并上报。
5. 每 500 ms 上报 ADC 和四路频率。
6. 每 500 ms 翻转运行指示灯,每 1 s 发送 CAN 心跳。
5. 每 500 ms 上报 ADC、每 20 ms 上报四路频率。
6. 每 500 ms 翻转运行指示灯,每 100 ms 发送 CAN 心跳。
`PE0` 为正常运行心跳灯;若进入 `Error_Handler()`,`PE0` 和 `PE1` 会快速同时翻转。
@@ -78,16 +78,18 @@ CAN1 和 CAN2 使用相同协议,所有主动上报均分别发送到两条总
| ID | 名称 | 周期/触发 | 数据 |
|---:|---|---|---|
| `0x6A0` | 心跳 | 每 1 s | `A1 01 seq_L seq_H uptime_0..3` |
| `0x6A0` | 心跳 | 每 100 ms | `A1 01 seq_L seq_H uptime_0..3` |
| `0x6A1` | ADC 电压 | 每 500 ms | 输入电压 `uint16`,单位 mV |
| `0x6A2` | 四路频率 | 每 500 ms | 4 个 `uint16`,单位 Hz,顺序 PA0/PA2/PA6/PB14 |
| `0x6A3` | DAC 设置命令 | 接收 | `[DAC 编号][mV_L][mV_H]` |
| `0x6A2` | 四路频率 | 每 20 ms | 4 个 `uint16`,单位 Hz,顺序 PA0/PA2/PA6/PB14 |
| `0x6A3` | DAC 设置命令 | 接收 | `[DAC 编号][mV_L][mV_H]`;每路在 100 ms 内未收到本路下一帧时归零 |
| `0x6A4` | DAC 应答 | 每次 DAC 设置后 | `[结果][DAC 编号][mV_L][mV_H]` |
| `0x6A5` | ASSI 灯控 | 接收 | `byte0`:`0=AS_OFF`、`1=AS_READY`、`2=AS_DRIVING`、`3=AS_EMERGENCY`、`4=AS_FINISHED` |
| `0x6A6` | GPIO 状态 | 上电、任一输入变化 | 状态 `uint16`、变化掩码 `uint16`、时间戳 `uint32 ms` |
心跳帧中板号为 `0xA1`、协议版本为 `0x01`,序号与上电时间均为低字节在前。完整信号定义和量纲以 [DBC 文件](SDC_ACQ_DAC.dbc) 为准。
DAC 的 CAN 安全超时按通道独立执行:收到 `0x6A3` 中 DAC1 或 DAC2 的有效控制帧后,该通道必须在 100 ms 内再次收到自己的控制帧,否则固件仅将该通道写为 0 mV 一次。串口 DAC 指令不启动或刷新这个 CAN 控制帧超时。
### ASSI RGB 状态灯
`PE9/PE10/PE11` 分别控制外部红、绿、蓝灯 MOS,均为高电平有效。`0x6A5` 可由 CAN1 或 CAN2 接收,状态在下一帧有效灯控报文到达前保持不变;上电默认 `AS_OFF`。`AS_READY` 为红绿常亮(黄),`AS_DRIVING` 为红绿以 2 Hz、50% 占空比闪烁(各 250 ms 亮/灭),`AS_EMERGENCY` 为蓝灯以相同节奏闪烁,`AS_FINISHED` 为蓝灯常亮。`PE0` 运行心跳和 `PE1` 故障指示保持原行为。