diff --git a/A11_F407_SDC_ACQ_DAC_CubeMX/Src/main.c b/A11_F407_SDC_ACQ_DAC_CubeMX/Src/main.c index 63fa698..675a3d4 100644 --- a/A11_F407_SDC_ACQ_DAC_CubeMX/Src/main.c +++ b/A11_F407_SDC_ACQ_DAC_CubeMX/Src/main.c @@ -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]; diff --git a/PC_Test/README.md b/PC_Test/README.md new file mode 100644 index 0000000..bb00745 --- /dev/null +++ b/PC_Test/README.md @@ -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。 diff --git a/PC_Test/SDCAcqDacTester/App.xaml b/PC_Test/SDCAcqDacTester/App.xaml new file mode 100644 index 0000000..21ed383 --- /dev/null +++ b/PC_Test/SDCAcqDacTester/App.xaml @@ -0,0 +1,6 @@ + + + diff --git a/PC_Test/SDCAcqDacTester/App.xaml.cs b/PC_Test/SDCAcqDacTester/App.xaml.cs new file mode 100644 index 0000000..465a634 --- /dev/null +++ b/PC_Test/SDCAcqDacTester/App.xaml.cs @@ -0,0 +1,5 @@ +using System.Windows; + +namespace SDCAcqDacTester; + +public partial class App : Application { } diff --git a/PC_Test/SDCAcqDacTester/Can/ControlCanTransport.cs b/PC_Test/SDCAcqDacTester/Can/ControlCanTransport.cs new file mode 100644 index 0000000..a324937 --- /dev/null +++ b/PC_Test/SDCAcqDacTester/Can/ControlCanTransport.cs @@ -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? FrameReceived; + public event EventHandler? 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 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(); +} diff --git a/PC_Test/SDCAcqDacTester/MainWindow.xaml b/PC_Test/SDCAcqDacTester/MainWindow.xaml new file mode 100644 index 0000000..b743123 --- /dev/null +++ b/PC_Test/SDCAcqDacTester/MainWindow.xaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + +