ccccccc@

  • 2025-04-13
  • 发表了主题帖: 【microchip PolarFire SoC FPGA 套件】 PolarFire SoC FPGA开发套件测评总结

       一、测评背景 PolarFire SoC FPGA开发套件功能丰富,能满足多种嵌入式系统开发需求。本次测评深入探究该套件实际使用情况,包括多种功能测试和开发流程体验,为开发者和学习者提供参考。    二、测评环境  (一)硬件环境 选用PolarFire SoC Discovery Kit(MPFS-DISCO-KIT)开发板,核心芯片是MPFS095T-1FCSG325E FPGA。开发板接口多,像microSD卡、DDR4、SGMII等接口,为测试提供了硬件支持。用USB 2.0 C型公对公屏蔽线来传输数据和供电,保证连接稳定。    (二)软件环境 主要用Microchip的Libero SoC软件来做项目开发、编译、引脚分配这些操作。调试嵌入式应用会用到SoftConsole,要是涉及相关功能,还得安装DirectCores和固件驱动等软件。    三、测评内容与结果  (一)基本功能测试 1. LED闪灭与呼吸灯功能     - LED闪灭:在Libero SoC软件里建工程,设置好引脚和逻辑,就能用按键控制LED亮灭。按硬件手册找到LED引脚信息(比如LED1连T18引脚,I/O标准是LVCMOS18),在软件里设置好,烧录程序后测试正常。     - 呼吸灯:做呼吸灯项目,要在Libero SoC软件里完成一系列操作。测试发现LED1有呼吸灯效果,但我偷懒了,实际亮度变化不太平滑,得调整频率设置(改COUNT_MAX参数值)来让效果更好。 2. UART功能:UART模块由TX、RX和TOP三个模块组成,支持115200bps波特率和50_000_000Hz时钟频率,用状态机收发数据。硬件连好后,用串口工具测试,TX、RX功能和双向通信都没问题。不过高速传输时,可能要优化硬件或软件,比如加CRC校验、扩FIFO深度或用中断驱动机制,让模块更可靠、吞吐量更大。 这些基础的功能使用现成的ip开发是相当的便捷通畅,上手也很快。  (二)网络功能测试 1. IP核选用尝试:测以太网接口时,对比了CoreRMII、CoreTSE等五个IP核,选了CoreTSE。但用的时候下载很慢,还老是卡死,而且报错引脚不够用。这个ip核的使用和选择能不能再简单直接一点? 2. 参考视频流传输方案测试:参考相关文档的视频流传输方案来搭建测试环境。硬件连好以太网接口模块,软件参考驱动和配置代码,添加测试数据相关逻辑。模拟测试显示,理想网络环境下,以太网接口通信稳定,丢包率不到1%,接近理论最大值。    四、开发流程体验  (一)软件安装与配置 去Microchip官网下载Libero SoC套件,安装时要选好路径和功能模块。装完后在Microchip Portal注册领免费Silver许可证。装SoftConsole要注意和Libero SoC版本兼容,DirectCores和固件驱动也要按文档指引下载安装。整个过程有点麻烦,版本兼容和注册领证的流程,新手可能会觉得难。    (二)项目创建与设计 在Libero SoC软件里创建项目,要设置项目名称、保存路径、选器件系列这些参数。创建SmartDesign文件并添加HDL文件后,进行引脚分配和设计验证。编译综合、布局布线和下载时,软件会生成报告,能帮开发者了解FPGA资源占用和时序性能,但报告信息多,新手不太好懂。    五、改进建议  (一)优化IP核 1. 针对CoreTSE等IP核下载的问题,Microchip要优化IP核和开发环境的兼容性,让下载更快更稳定。 2. 在IP核设计上,增加引脚数量或者让引脚配置更灵活,满足不同测试和开发的需求。    (二)简化软件流程 1. 把软件安装和配置过程简化,减少麻烦的步骤和版本兼容问题。比如安装Libero SoC套件时,自动安装兼容的SoftConsole等软件,简化领证流程,给更清楚的指引。 2. 优化Libero SoC软件的界面和报告显示,把编译综合、布局布线的报告简化,突出重点,让新手更容易看懂。    (三)完善文档资料 提供更详细、好懂的文档。介绍IP核时,多给实际案例和使用教程,方便开发者选择和使用。开发流程的文档,每个步骤都配上操作截图和说明,特别是软件配置和参数设置部分,让使用者不那么困惑。

  • 发表了主题帖: 【microchip PolarFire SoC FPGA 套件】PolarFire SoC FPGA 开发套件网络功能的尝试

      一、前期尝试:FPGA手工开发与IP核选用困境 最初,考虑直接使用FPGA进行以太网接口测试开发。然而,纯手工编写代码实现测试功能面临诸多挑战,开发周期长且容易出错,代码的复杂性和调试难度使得这一方案性价比极低。因此,转向使用官方提供的IP核,期望借此简化开发流程。 在众多IP核中,有CoreRMII、CoreTSE、CoreTSE_AHB、Core10100和CoreTCAM这五个模块可供选择。对这五个IP核的详细对比如下: IP核名称 功能特点 优势 局限 适用场景 CoreRMII 负责MII到RMII的转换,减少连接PHY和MAC的信号数量;支持10/100Mbps模式,MII侧25MHz、RMII侧50MHz时钟;提供MDIO接口控制链路速度、传输类型和环回功能 资源占用低,在SmartFusion2、IGLOO2、PolarFire等器件系列中,逻辑元件使用数量少 功能单一,专注于接口转换,不支持1000Mbps高速传输模式 对接口转换需求简单,速率要求不高于100Mbps,且对资源占用敏感的场景 CoreTSE 提供10/100/1000Mbps以太网MAC功能,支持G/MII或SGMIITBI接口;支持全双工和半双工模式,具备WoL功能、帧统计计数器和目的地址过滤功能 功能丰富,能满足多种以太网应用需求;支持高速传输,适用于对网络功能要求较高的场景 资源占用相对较高;配置和使用复杂,对开发者技术要求高 对功能完整性和高速传输有要求,资源相对充足的项目,如工业控制、数据中心网络设备等 CoreTSE_AHB 提供10/100/1000Mbps以太网MAC功能,支持G/MII和TBI接口;通过AMBAAHB-发起端口接口,允许DMA引擎进行数据传输,提高效率 通过AHB接口实现高效数据传输和系统集成;在数据传输方面具有优势 硬件连接和软件配置复杂,开发难度大 对数据传输效率要求高,需要与AHB总线紧密集成的系统,如高性能计算设备的网络模块 Core10100 推测主要针对10/100Mbps以太网应用场景设计优化 在资源占用和成本控制上或许有优势 功能丰富度和高速传输支持较弱;缺乏详细资料,实际性能和功能特点难以准确评估 对成本敏感,对功能需求相对简单,速率要求不高于100Mbps的场景,如一些低成本物联网设备 CoreTCAM 推测与内容可寻址存储器(TCAM)技术相关,可能在地址查找、数据包过滤方面有独特优势 在需要快速进行地址匹配和数据过滤的场景中表现出色 缺乏详细资料,功能细节和性能表现难以明确 对地址查找和数据包过滤速度要求极高的特定网络应用,如网络安全设备中的数据包筛选模块 综合考虑项目需求,包括对多种网络环境的适应性、大数据传输的稳定性以及开发效率,最终选择了CoreTSE。尽管其资源占用较大,但全面的功能能更好地满足不同网络环境下的测试需求,且后续可通过优化资源配置来缓解资源压力。 在使用CoreTSE的过程中,却遇到了新的问题。下载过程极为缓慢,频繁出现卡死现象,严重影响开发进度。经过排查,发现是由于IP核数据量较大,在下载过程中与开发环境存在兼容性问题(我的电脑性能不足或者网络原因?)。 这里的配置就按照默认的就可以了。 好不容易完成下载,又出现引脚太少无法满足测试需求的状况,这使得基于该IP核的测试方案难以继续推进。 二、新的探索:参考视频流传输方案 在困境中,通过仔细研读PolarFireSoCFPGA相关手册,发现其中的视频流传输方案可能为以太网接口测试提供新的思路。视频流传输同样涉及大量数据在网络中的传输,与以太网接口测试在数据传输特性上有一定相似性。 在困境中,通过仔细研读PolarFireSoCFPGA相关手册,发现其中的视频流传输方案可能为以太网接口测试提供新的思路。视频流传输同样涉及大量数据在网络中的传输,与以太网接口测试在数据传输特性上有一定相似性。 从《PolarFir eSoCFPGAH.264 Video Streamin g Over Ethernet Application Note AN4529》文档可知,视频流传输方案利用了一系列IP块,如PolarFireOscillator、PolarFire ClockConditioning Circuit(CCC)等协同工作。其中,网络通信部分的设计对以太网接口测试有重要参考价值。在时钟管理方面,通过合理配置PolarFireCCC块,利用148.5MHz的板载参考时钟生成125MHz的织物时钟,为FPGA与MSS(Microprocessor Subsystem)之间的通信提供稳定时钟信号。这在以太网接口测试中也至关重要,稳定的时钟是保证数据准确传输、测试通信稳定性和速率的基础。 同时,该方案中的数据传输路径设计,如从摄像头获取数据后经一系列处理再通过以太网发送,为测试数据的传输提供了可借鉴的流程。在测试以太网接口时,也需要构建类似的数据传输链路,将测试数据准确发送并接收,以分析丢包率等指标。 基于此,确定了工程结构。在硬件层面,以PolarFireSoC FPGA为核心,连接以太网接口模块,确保数据的输入输出稳定。在软件层面,参考视频流传输方案中的驱动和配置代码,构建测试数据的生成、发送和接收逻辑。 需要添加的代码主要集中在测试数据的生成函数、数据发送与接收的控制逻辑以及丢包率和传输速率计算的函数。 例如,编写专门的函数用于生成不同大小和格式的测试数据包,模拟大数据传输场景;在数据发送端,根据不同网络环境的模拟需求,添加控制数据包发送频率和数量的代码;在接收端,增加对接收数据的校验和计数功能,以便准确计算丢包率。同时,结合时钟配置代码,确保数据在不同网络环境下能稳定传输。 下载官方用例:gitclonehttps://github.com/polarfire-soc/polarfire-soc-bare-metal-examples.git 参考里面的mpfs-mac-mcc-stack项目和mpfs-mac-simple-test-multi-emac项目。 在mpfs-mac-mcc-stack项目中,包含了BACnet协议相关的测试函数,BACnet是一种用于楼宇自动化和控制网络的通信协议,在以太网通信测试中可能会涉及到一些通用的网络通信功能。 比如在src/middleware/bacnet/src/npdu.c文件,包含了testNPDU1和testNPDU2函数,这些函数用于测试网络协议数据单元(NPDU)的编码和解码功能,涉及到以太网通信中的数据包处理。 有兴趣的可以在工程里面看一下testNPDU1和testNPDU2两函数。 而位于src/middleware/bacnet/src/ptransfer.c,代码包含了test_Private_Transfer_Request和test_Private_Transfer_Ack函数,用于测试私有传输请求和响应的编码和解码,有助于理解以太网通信中的消息交互。 最后是文件src/middleware/bacnet/src/mstp.c,它包含了testReceiveNodeFSM函数,用于测试MSTP(主从/令牌传递)网络的接收节点有限状态机,对于理解以太网通信中的状态管理和数据包接收处理有帮助。 PPP协议参考driver-examples/mss/mss-ethernet-mac/mpfs-uart-mac-freertos_lwip/src/middleware/lwip-2.0.0-wip/netif/ppp/ppp.c。 数据传输参考:applications/benchmarks/dma_benchmarking/mpfs-dma-benchmarking/src/application_pdma/hart1/pdma_benchmarking_config.h。定义了 DMA(直接内存访问)基准测试的配置,包括传输大小范围、内存地址等。虽然这主要是关于 DMA 的配置,但在大数据传输速率测试中,DMA 可以用于高效的数据传输。 进行一个简单的功能代码的编写: #define BUFFER_SIZE 1024 #define PORT 8888 #define SERVER_IP "192.168.68.74" #define PACKET_COUNT 1000 typedef struct { uint8_t protocol_version; bool data_expecting_reply; bool network_layer_message; BACNET_MESSAGE_PRIORITY priority; BACNET_NETWORK_MESSAGE_TYPE network_message_type; uint16_t vendor_id; uint8_t hop_count;} BACNET_NPDU_DATA; void npdu_copy_data(BACNET_NPDU_DATA * dest, BACNET_NPDU_DATA * src) { if (dest && src) { dest->protocol_version = src->protocol_version; dest->data_expecting_reply = src->data_expecting_reply; dest->network_layer_message = src->network_layer_message; dest->priority = src->priority; dest->network_message_type = src->network_message_type; dest->vendor_id = src->vendor_id; dest->hop_count = src->hop_count; }} int send() { int sockfd; struct sockaddr_in server_addr; uint8_t buffer[1024]; Packet packet; int i; clock_t start_time, end_time; double total_time; sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("socket creation failed"); return -1; } memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); start_time = clock(); for (i = 0; i < PACKET_COUNT; i++) { packet.packet_id = i; packet.timestamp = (uint64_t)time(NULL); snprintf((char *)packet.data, sizeof(packet.data), "Packet %d", i); packet.data_length = strlen((char *)packet.data); int encoded_length = encode_packet(buffer, &packet); sendto(sockfd, buffer, encoded_length, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)); usleep(1000); } end_time = clock(); total_time = (double)(end_time - start_time) / CLOCKS_PER_SEC; printf("Sent %d packets in %.2f seconds.\n", PACKET_COUNT, total_time); close(sockfd); return 0; } #define BUFFER_SIZE 1024 #define PORT 8888 int receive() { int sockfd; struct sockaddr_in server_addr, client_addr; socklen_t client_addr_len = sizeof(client_addr); uint8_t buffer[1024]; Packet packet; int packet_count = 0; int received_count = 0; sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("socket creation failed"); return -1; } memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind failed"); return -1; } while (1) { ssize_t recv_len = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_addr_len); if (recv_len > 0) { decode_packet(buffer, &packet); received_count++; printf("Received packet %d at %lu: %s\n", packet.packet_id, packet.timestamp, packet.data); if (packet.packet_id == MAX_PACKET_ID) { break; } } } int lost_count = MAX_PACKET_ID - received_count + 1; float loss_rate = (float)lost_count / (MAX_PACKET_ID + 1) * 100; printf("Received %d packets, lost %d packets, loss rate: %.2f%%\n", received_count, lost_count, loss_rate); close(sockfd); return 0;} int encode_packet(uint8_t * buffer, Packet * packet) { uint8_t * ptr = buffer; *(uint32_t *)ptr = packet->packet_id; ptr += sizeof(uint32_t); *(uint64_t *)ptr = packet->timestamp; ptr += sizeof(uint64_t); *(uint16_t *)ptr = packet->data_length; ptr += sizeof(uint16_t); for (int i = 0; i < packet->data_length; i++) { *ptr++ = packet->data; } return ptr - buffer; } int decode_packet(uint8_t * buffer, Packet * packet) { uint8_t * ptr = buffer; packet->packet_id = *(uint32_t *)ptr; ptr += sizeof(uint32_t); packet->timestamp = *(uint64_t *)ptr; ptr += sizeof(uint64_t); packet->data_length = *(uint16_t *)ptr; ptr += sizeof(uint16_t); for (int i = 0; i < packet->data_length; i++) { packet->data = *ptr++; } return ptr - buffer; } 模拟测试结果与分析 对以太网接口进行模拟测试。在不同网络环境下,包括不同带宽、不同干扰强度的模拟场景中,测试结果显示:在理想网络环境下,即高带宽且低干扰的场景中,以太网接口的通信稳定性良好,丢包率低于1%,大数据传输速率可达到接近理论最大值。

  • 发表了主题帖: 【microchip PolarFire SoC FPGA 套件】DS18B20 温度测量与串口传输

      一、测评目标 本次测评旨在使用开发板连接DS18B20温度传感器(转接板自带上拉电阻,所以连接开发板就行了),实现温度测量功能,并将测量结果通过串口经USB转ttl传输到上位机。 二、实现思路 硬件连接 DS18B20传感器通过单总线协议与开发板连接,需要将DS18B20的数据引脚连接到开发板的一个GPIO引脚。同时,开发板的串口通过USB转ttl模块与上位机连接,以便将温度数据传输到上位机进行显示。 软件实现 DS18B20驱动:实现DS18B20的初始化、复位、读写操作,以及温度数据的读取和转换。 串口通信:初始化串口,设置波特率、数据位、停止位等参数,将DS18B20读取的温度数据通过串口发送到上位机。 主程序:在主循环中,周期性地读取DS18B20的温度数据,并将其通过串口发送出去。 三、代码实现 在实现过程中,尝试参考了仓库中的代码,可以参考仓库中的print_help 函数示例(polarfire-soc-bare-metal-examples/driver-examples/mss/mss-ethernet-mac/mpfs-mac-mcc-stack/src/application/hart0/e51.c 和 polarfire-soc-bare-metal-examples/driver-examples/mss/mss-ethernet-mac/mpfs-mac-simple-test-multi/src/application/hart0/e51.c)给了关于字符串格式化和打印输出的一些启发,让了解了如何使用 sprintf 和 PRINT_STRING 进行信息输出。 DS18B20部分驱动代码 include "mpfs_hal/mss_hal.h" void ds18b20_reset(void) { MSS_GPIO_set_output(DS18B20_GPIO_PIN, 0); MSS_Delay(500); MSS_GPIO_set_output(DS18B20_GPIO_PIN, 1); MSS_Delay(60); if (MSS_GPIO_get_input(DS18B20_GPIO_PIN) == 0) { } else { } } void ds18b20_write_byte(uint8_t byte) { for (int i = 0; i < 8; i++) { if ((byte & 0x01) == 0x01) { MSS_GPIO_set_output(DS18B20_GPIO_PIN, 0); MSS_Delay(6); MSS_GPIO_set_output(DS18B20_GPIO_PIN, 1); MSS_Delay(64); } else { MSS_GPIO_set_output(DS18B20_GPIO_PIN, 0); MSS_Delay(60); MSS_GPIO_set_output(DS18B20_GPIO_PIN, 1); MSS_Delay(10); } byte >>= 1; } } uint8_t ds18b20_read_byte(void) { uint8_t byte = 0; for (int i = 0; i < 8; i++) { MSS_GPIO_set_output(DS18B20_GPIO_PIN, 0); MSS_Delay(6); MSS_GPIO_set_output(DS18B20_GPIO_PIN, 1); MSS_Delay(9); byte >>= 1; if (MSS_GPIO_get_input(DS18B20_GPIO_PIN) == 1) { byte |= 0x80; } MSS_Delay(55); } return byte; } float ds18b20_read_temperature(void) { ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0x44); MSS_Delay(750); ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); uint8_t low_byte = ds18b20_read_byte(); uint8_t high_byte = ds18b20_read_byte(); int16_t raw_temperature = (high_byte << 8) | low_byte; float temperature = (float)raw_temperature / 16.0; return temperature; } 串口通信部分代码 include "mpfs_hal/mss_hal.h" include "mss_uart.h" void uart_init(void) { MSS_UART_init(&g_mss_uart0, MSS_UART_115200_BAUD, MSS_UART_DATA_8_BITS | MSS_UART_NO_PARITY | MSS_UART_ONE_STOP_BIT); } void uart_send_string(const char str) { while (str) { MSS_UART_polled_tx(&g_mss_uart0, str++); } } void uart_send_float(float value) { char buffer[20]; sprintf(buffer, "%.2f", value); uart_send_string(buffer); } 主程序 include "mpfs_hal/mss_hal.h" include "mss_uart.h" include "ds18b20.h" include "uart.h" int main(void) { uart_init(); while (1) { float temperature = ds18b20_read_temperature(); uart_send_string("Temperature: "); uart_send_float(temperature); uart_send_string(" C\n"); MSS_Delay(1000); } return 0; } 将传感器放入室温的水杯中,进行测试,等待一定时间后,查看打印。 四、遇到的问题及解决方法 DS18B20通信失败 - 问题描述:在初始化或读取DS18B20数据时,无法得到正确的应答信号或读取到的数据错误。 - 可能原因:硬件连接不稳定、时序问题、电源问题等。 - 解决办法:首先检查了DS18B20的数据引脚和电源引脚,发现有一处引脚松动,重新连接后,仍未解决问题。接着,仔细研究DS18B20的时序要求,调整了复位和读写操作的延时时间,最终成功得到了正确的应答信号和数据。 串口通信乱码 - 问题描述:通过串口发送到上位机的数据显示为乱码。 - 可能原因:波特率设置不一致、数据位或停止位设置错误、串口驱动问题等。 - 解决办法:检查了开发板和上位机的串口设置,发现上位机的波特率设置与代码中不一致,将上位机的波特率调整为115200后,乱码问题得到解决。 温度数据不准确 - 问题描述:读取到的DS18B20温度数据与实际温度相差较大。 - 可能原因:DS18B20校准问题、环境干扰等。 - 解决办法:尝试对DS18B20进行校准,根据实际情况调整温度转换公式,但仍存在一定误差。后续将进一步检查传感器周围的环境,避免干扰源,以提高温度数据的准确性。

  • 2025-03-26
  • 回复了主题帖: >>征集 | 晒电机控制痛点与难题,一起寻求最优解!

    在电机控制设计中,面临诸多痛点难题:1.高精度控制时,传感器信号易受干扰,影响电机定位、速度和扭矩控制精度,如工业机械臂控制受干扰后抓取位置偏差。2.实时性方面,复杂算法对处理器性能要求高,普通MCU难以满足,像无人机飞行控制中电机响应滞后影响飞行。3.热管理和功耗也很关键,电动汽车电机高负载运行发热量大,传统散热不佳会减短续航、引发故障。4.电磁兼容性上,智能家居中电机驱动干扰无线通信。5.电机参数随温度、老化变化,工业风机电机电阻变化会使风量不稳定。而ADI的TMC9660芯片在这些方面表现出色,它能优化传感器信号处理、高效处理复杂算法、集成低损耗驱动并智能温控、降低EMI辐射、自动识别并适应电机参数变化,有效解决上述难题。

  • 2025-03-17
  • 发表了主题帖: 【microchip PolarFire SoC FPGA 套件】简单uart的实现

    在进行 UART 模块功能测评时,首先需要搭建测试环境。硬件方面通过usb转ttl连接开发板的 TX、RX、GND 引脚至电脑 USB 接口,使用 USB Type-C 线供电并确保 J47 跳线默认连接 USB Type-C 端口。使用串口助手进行测试。 UART 模块由 TX、RX 和 TOP 三个独立模块组成。 TX 模块负责将并行数据转换为串行数据发送。代码中通过参数化设计,支持 115200bps 的波特率,时钟频率为 50_000_000Hz。它采用状态机实现数据发送流程,定义了五个状态:TX_IDLE(空闲状态)、TX_START(起始位发送状态)、TX_DATA(数据位发送状态)、TX_STOP(停止位发送状态)和 TX_CLEAN(清理状态)。 在空闲状态下,若 tx_en 信号有效,则将待发送的并行数据 tx_data 存入 tx_shift_reg,并进入起始位发送状态,将 tx 信号拉低表示起始位。在起始位和数据位发送状态中,通过 tx_baud_cnt 计数器对时钟进行计数,当计数值达到 BAUD_DIV(时钟频率与波特率的比值)时,进行相应的状态转换和数据移位操作。在停止位发送状态,将 tx 信号拉高,发送停止位。最后在清理状态将 tx_done 信号置高,表示发送完成,并回到空闲状态。 module uart_tx #( parameter BAUD_RATE = 115200, parameter CLK_FREQ = 50_000_000 ) ( input wire clk, input wire rst_n, input wire [7:0] tx_data, input wire tx_en, output reg tx, output reg tx_done ); localparam BAUD_DIV = CLK_FREQ / BAUD_RATE; localparam [2:0] TX_IDLE = 3'b000, TX_START = 3'b001, TX_DATA = 3'b010, TX_STOP = 3'b011, TX_CLEAN = 3'b100; reg [2:0] tx_state; reg [15:0] tx_baud_cnt; reg [3:0] tx_bit_cnt; reg [7:0] tx_shift_reg; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin tx_state <= TX_IDLE; tx_baud_cnt <= 0; tx_bit_cnt <= 0; tx_shift_reg <= 0; tx <= 1'b1; tx_done <= 0; end else begin case (tx_state) TX_IDLE: begin tx_done <= 0; if (tx_en) begin tx_shift_reg <= tx_data; tx_bit_cnt <= 0; tx_baud_cnt <= 0; tx_state <= TX_START; tx <= 0; end end TX_START: begin if (tx_baud_cnt == BAUD_DIV - 1) begin tx_baud_cnt <= 0; tx_state <= TX_DATA; end else begin tx_baud_cnt <= tx_baud_cnt + 1; end end TX_DATA: begin if (tx_baud_cnt == BAUD_DIV - 1) begin tx <= tx_shift_reg[0]; tx_shift_reg <= tx_shift_reg >> 1; tx_baud_cnt <= 0; if (tx_bit_cnt == 7) begin tx_bit_cnt <= 0; tx_state <= TX_STOP; end else begin tx_bit_cnt <= tx_bit_cnt + 1; end end else begin tx_baud_cnt <= tx_baud_cnt + 1; end end TX_STOP: begin tx <= 1; if (tx_baud_cnt == BAUD_DIV - 1) begin tx_state <= TX_CLEAN; tx_baud_cnt <= 0; end else begin tx_baud_cnt <= tx_baud_cnt + 1; end end TX_CLEAN: begin tx_done <= 1; tx_state <= TX_IDLE; end default: tx_state <= TX_IDLE; endcase end end endmodule RX 模块将串行数据转换为并行数据接收。同样采用参数化设计,支持 115200bps 波特率和 50_000_000Hz 时钟频率。使用状态机实现接收流程,定义了五个状态:RX_IDLE(空闲状态)、RX_START(起始位检测状态)、RX_DATA(数据位接收状态)、RX_STOP(停止位接收状态)和 RX_CLEAN(清理状态)。 在空闲状态下,若检测到 rx 信号变低(起始位),则将 rx_baud_cnt 计数器初始化为 BAUD_DIV / 2,进入起始位检测状态。在起始位检测状态中,若在中间时刻 rx 信号仍为低,则确认起始位有效,进入数据位接收状态。在数据位接收状态,通过 rx_baud_cnt 计数器计数,当计数值为 0 时,将 rx 信号的值存入 rx_shift_reg 相应位。在停止位接收状态,若计数完成,则将 rx_shift_reg 的值赋给 rx_data,并将 rx_done 信号置高,表示接收完成,最后进入清理状态回到空闲状态。 module uart_rx #( parameter BAUD_RATE = 115200, parameter CLK_FREQ = 50_000_000 ) ( input wire clk, input wire rst_n, input wire rx, output reg [7:0] rx_data, output reg rx_done ); localparam BAUD_DIV = CLK_FREQ / BAUD_RATE; localparam [2:0] RX_IDLE = 3'b000, RX_START = 3'b001, RX_DATA = 3'b010, RX_STOP = 3'b011, RX_CLEAN = 3'b100; reg [2:0] rx_state; reg [15:0] rx_baud_cnt; reg [3:0] rx_bit_cnt; reg [7:0] rx_shift_reg; reg rx_sync; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rx_sync <= 1; end else begin rx_sync <= rx; end end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rx_state <= RX_IDLE; rx_baud_cnt <= 0; rx_bit_cnt <= 0; rx_shift_reg <= 0; rx_data <= 0; rx_done <= 0; end else begin case (rx_state) RX_IDLE: begin rx_done <= 0; if (!rx_sync) begin rx_baud_cnt <= BAUD_DIV / 2; rx_state <= RX_START; end end RX_START: begin if (rx_baud_cnt == 0) begin if (!rx_sync) begin rx_baud_cnt <= BAUD_DIV - 1; rx_state <= RX_DATA; rx_bit_cnt <= 0; end else begin rx_state <= RX_IDLE; end end else begin rx_baud_cnt <= rx_baud_cnt - 1; end end RX_DATA: begin if (rx_baud_cnt == 0) begin rx_shift_reg[rx_bit_cnt] <= rx_sync; rx_baud_cnt <= BAUD_DIV - 1; if (rx_bit_cnt == 7) begin rx_state <= RX_STOP; end else begin rx_bit_cnt <= rx_bit_cnt + 1; end end else begin rx_baud_cnt <= rx_baud_cnt - 1; end end RX_STOP: begin if (rx_baud_cnt == 0) begin rx_data <= rx_shift_reg; rx_done <= 1; rx_state <= RX_CLEAN; end else begin rx_baud_cnt <= rx_baud_cnt - 1; end end RX_CLEAN: begin rx_state <= RX_IDLE; end default: rx_state <= RX_IDLE; endcase end end endmodule TOP 模块整合了 TX 和 RX 模块,实现双向通信控制。它支持中断或轮询模式,通过关键信号 TXD、TX_EN、TX_DONE、RXD、RX_VALID 和 RX_OVERFLOW 进行通信控制。在代码中,当 rx_done 信号有效时,将接收到的并行数据 rx_data 赋值给 tx_data_reg,同时使能 tx_en 信号,触发 TX 模块将接收到的数据原样发送回去,实现双向通信 module top_uart ( input wire clk, input wire rst_n, input wire rx, output wire tx ); wire [7:0] rx_data; wire rx_done; wire tx_done; wire tx_en; reg [7:0] tx_data_reg; // 当接收到数据完成时,将接收到的数据赋值给发送数据寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin tx_data_reg <= 8'b0; end else if (rx_done) begin tx_data_reg <= rx_data; end end // 当接收到数据完成时,使能发送模块 assign tx_en = rx_done; uart_tx #( .BAUD_RATE(115200), .CLK_FREQ(50_000_000) ) uart_tx_inst ( .clk(clk), .rst_n(rst_n), .tx_data(tx_data_reg), .tx_en(tx_en), .tx(tx), .tx_done(tx_done) ); uart_rx #( .BAUD_RATE(115200), .CLK_FREQ(50_000_000) ) uart_rx_inst ( .clk(clk), .rst_n(rst_n), .rx(rx), .rx_data(rx_data), .rx_done(rx_done) ); endmodule 对于引脚的设置,参照手册如下: 硬件连接完成后,打开串口工具选择正确的 COM 端口,设置波特率为 115200bps、数据位 8 位、停止位 1 位、无校验及无流控。功能测试时,通过 TOP 模块发送预设数据 “test”,串口工具正确显示发送的数据且 TX_DONE 信号在发送完成后置高,验证了 TX 功能;通过串口工具发送数据 “Test RX”,TOP 模块成功接收数据且 RX_VALID 信号有效,验证了 RX 功能;双向通信测试中,TOP 模块与串口工具间数据传输无误,双向通信正常。 本次测评结果表明,UART 模块实现了基本的 TX、RX 功能,支持双向通信,在标准波特率下稳定运行,但高速传输时需优化硬件或软件配置。后续可通过添加 CRC 校验、扩展 FIFO 深度或采用中断驱动机制提升模块的可靠性和吞吐量。

  • 2025-03-06
  • 发表了主题帖: 【microchip PolarFire SoC FPGA 套件】led呼吸灯实现

    一、测评环境 本次测评选用 PolarFire SoC Discovery Kit(MPFS-DISCO-KIT)开发板,其核心芯片为 MPFS095T-1FCSG325E FPGA。开发板上的 50MHz 振荡器通过 R18 引脚为设计中的 clk 提供时钟信号,而 LED1 连接到 T18 引脚,用于直观呈现呼吸灯效果。此外,开发板丰富的接口资源为后续功能扩展提供了基础条件。 二、测评步骤 (一)创建项目 打开 Libero SoC 软件,选择 “Project> New Project” 选项,开始新建项目。 在弹出的 “Project Details” 页面中,填写项目名称、选择合适的项目保存路径,并指定 “Preferred HDL Type” 为 Verilog。 进入 “Device Selection” 页面,依次选择 “Family” 为 PolarFireSoC,“Die” 为 MPFS095T,“Package” 为 FCSG325,“Speed” 为 - 1,“Range” 为 EXT。 在 “Device Settings” 页面,保持默认的关键设置不变,最后点击 “Finish” 完成项目创建。 (二)添加代码与设计文件 在项目中创建新的 Verilog 文件,命名为 “breathing_led.v”,并将给定的呼吸灯代码粘贴到该文件中保存。代码如下: verilog module breathing_led ( input wire clk, output reg led); parameter COUNT_MAX = 16667;parameter STEP = 100; reg [23:0] counter;reg [23:0] duty_cycle;reg up_down; always @(posedge clk) begin if (counter < COUNT_MAX - 1) begin counter <= counter + 1; end else begin counter <= 0; if (up_down == 0) begin if (duty_cycle < COUNT_MAX - STEP) begin duty_cycle <= duty_cycle + STEP; end else begin duty_cycle <= COUNT_MAX - STEP; up_down <= 1; end end else begin if (duty_cycle > STEP) begin duty_cycle <= duty_cycle - STEP; end else begin duty_cycle <= STEP; up_down <= 0; end end end if (counter < duty_cycle) begin led <= 0; end else begin led <= 1; endend endmodule 创建 SmartDesign 文件:选择 “File> New > SmartDesign”,输入合适的名称(如 “breathing_led_smart”)并点击 “OK”。 在 SmartDesign 中添加 HDL 文件:在 Libero SoC 的 IP catalog 中找到并拖入 “breathing_led.v” 对应的 HDL 文件到 SmartDesign 画布中。 设置引脚并连接到上层:在 SmartDesign 画布中,选中 “breathing_led” 模块的相关引脚(如 clk 和 led),右键选择 “Promote to Top Level”,将这些引脚连接到上层,以便后续进行引脚分配和设计验证。 仿真验证:利用 Libero SoC 的仿真工具,对设计进行仿真验证。设置合适的仿真激励,观察输出结果是否符合预期,检查代码功能是否正确实现。 (三)编译综合 在 “Design Flow” 窗口中,展开 “Implement Design”,右键点击 “Synthesize” 并选择 “Run”。编译综合过程完成后,仔细查看编译综合信息和资源使用报告,评估设计对 FPGA 资源的占用情况,判断是否满足项目需求。 (四)引脚分配 在 “Design Flow” 窗口中,展开 “Constraints”,右键点击 “Manage Constraints”,选择 “Open Manage Constraints View”。 在弹出的 “Constraint Manager” 窗口中,选择 “I/O Attributes” 选项卡,点击 “Edit with I/O Editor”。 在 “I/O Editor” 的 “Port View” 页面,依据开发板指南,将 “clk” 引脚连接到 R18 引脚,“led” 引脚连接到 T18 引脚,并设置正确的 I/O 标准。完成设置后,选择 “File > Commit and Check”,确保引脚分配无误。 (五)布局布线 引脚分配完成后,在 “Design Flow” 窗口中,右键点击 “Place and Route” 并选择 “Run”。布局布线完成后,查看布局布线报告中的时序和信号延迟信息,评估设计的时序性能是否满足要求。 (六)下载文件 布局布线完成后,在 “Design Flow” 窗口中,展开 “Program Design”,右键点击 “Run PROGRAM Action” 并选择 “Run”。通过 FlashPro Express 软件将生成的文件下载到开发板中,下载过程中确保开发板连接正常且供电稳定,切勿打断下载操作。 (七)功能测试 观察开发板上 LED1 的状态,正常情况下应呈现呼吸灯效果,留意亮度变化是否平滑。 三、测评结果分析 (一)功能实现 LED1 呈现出呼吸灯效果,但亮度变化不够平滑,这表明代码功能虽已实现,但频率设置可能需要进一步调整优化,以达到更理想的视觉效果。 (二)频率准确性 将测量得到的频率与 1500Hz 进行对比,若偏差在 ±10% 范围内,则可认为频率满足要求。若偏差较大,当实际频率高于 1500Hz 时,适当增大代码中的 COUNT_MAX 参数值;若实际频率低于 1500Hz,则适当减小 COUNT_MAX 参数值。调整参数后,需重新进行编译、下载和测试,直至频率达到满意的精度范围。 [localvideo]f3276c0c3389b7051b3cdb05f3318cba[/localvideo]  

  • 2025-02-27
  • 回复了主题帖: 【microchip PolarFire SoC FPGA 套件】②并不算一帆风顺的点灯之旅

    这个板子还是很强大的,不过阅读英语比较吃力的话,可以试试ai(不过ai也会有些问题存在,比如乱回复,回答没有重点)

  • 发表了主题帖: 【microchip PolarFire SoC FPGA 套件】led闪灭实现

    硬件连接:开发板提供了八个低电平有效 LED(LED1 - LED8),用于调试应用,这些 LED 连接到 PolarFire SoC FPGA。其中 LED1 连接到 FPGA 引脚 T18。 工程创建与设置 启动 Libero SoC 软件,创建新工程,在相应对话框中依次设置工程名称,首选 Verilog,并在后续页面选择器件系列等参数,最后点击 “Finish” 完成工程创建。 打开 SmartDesign 画布,输入合适名称创建新设计,展开 Libero SoC IP 目录中的 Macro Library,拖放相关逻辑元件到画布并按设计思路连接配置。 引脚分配与约束配置 查阅文档确定 LED 对应的 FPGA 引脚信息及 I/O 标准,如 LED1 的引脚为 T18,I/O 标准为 LVCMOS18。 在硬件手册上可以得知,两个按键的引脚名称。通过和led1进行连接,便可以进行亮灭控制。 在软件中进入 Design Flow 窗口,展开 Constraints,右键点击 “Manage Constraints” 并选择 “Open Manage Constraints View”;在 Constraint Manager 窗口选择 “I/O Attributes” 选项卡,点击 “Edit with I/O Editor”; 于 I/O Editor 页面的 “Port View” 选项卡设置 LED 引脚的 I/O Standard 和 Pin Number,可点击 “Unassigned” 查看可用引脚后手动输入或选择,完成后选择 “I/O Attribute Editor” 菜单的 “File > Commit and Check” 检查错误确保连接匹配。 烧录和测试 最后点击进行编程烧录,注意不要打断。 最后就可以进行按键控制亮灭了。使用现成的ip内核进行开发是非常方便又快捷的。 进度条跑完,出现一个小绿勾就是烧写完毕了。

  • 发表了主题帖: 【microchip PolarFire SoC FPGA 套件】PolarFire SoC FPGA 开发套件开箱测评

    一、开箱展示 PolarFire SoC FPGA 开发套件包装防护到位。打开包装,开发板置于海绵凹槽中,核心 MPFS095T - 1FCSG325E FPGA 芯片醒目,周边接口与元件布局有序。套件附带 USB 2.0 C 型公对公屏蔽线,线质好保障数据传输;Quickstart 卡标注基本连接与操作步骤,方便上手。 二、开发环境搭建 (一)软件安装配置 首先前往官网进行下载,可以直接进行搜索或者点击手册中的超链接; https://www.microchip.com/en-us/products/fpgas-and-plds/fpga-and-soc-design-tools/fpga/libero-software-later-versions 我前面就是绕了弯路,下载的其他版本,创建工程时候超不到开发板对应的参数; 然后按向导安装 Libero® SoC 套件,除了没有安装在C盘,其他的我都是默认选项,其自带编程驱动。安装时依系统版本和开发目标选路径与功能模块。同时安装 SoftConsole 用于调试,注意与 Libero® SoC 版本兼容。若用 DirectCores 和固件驱动,按文档指引分别下载安装; 然后在 Microchip Portal 注册获免费 Silver 许可证,如果没有账号的,用邮箱注册一个; https://www.microchip.com/en-us/products/fpgas-and-plds/fpga-and-soc-design-tools/fpga/licensing 然后选择Libero Silver 1 Yr DiskID NL License; 需要输入C盘的识别符号,我是在WINDOWS平台新进行开发的,在终端里输入“vol c:“就可以获得c盘的识别符了; lisces会在30分钟内发送到你的邮箱中,接着就可以点开软件,选择lisces所在路径,就能进行使用了; (二)硬件连接设定 开发板可通过 Type - C 端口或 5V DC 插孔供电,默认 J47 跳线 1、2 引脚闭合由 Type - C 供电,用 5V DC 插孔则打开。我是直接连接在电脑usb端进行供电的 三、工程创建流程 完成环境搭建后,用 Libero 软件编程。 点击 “New” 或 “Project > New Job Project” 创建项目,指定编程作业文件路径与保存位置。 选文件要适配目标设备与需求。点击 “finish”,确认编程器编号,完成初步设置。 准备好后点击 “RUN” 编程,成功显示 “RUN PASSED” 即可后续开发测试。

  • 2025-02-06
  • 回复了主题帖: 测评入围名单: PolarFire SoC FPGA Discovery 套件

    确认可完成测评计划

  • 2024-11-27
  • 回复了主题帖: 免费下载 | 安森美电动汽车充电白皮书,看碳化硅如何缓解“里程焦虑”!

    已经参加

  • 2024-11-11
  • 发表了主题帖: STM32H7S78-DK 开发套件三周目评测:简单声音采集保存之SD 卡录音保存和状态显示

    最后一个测试周目是基于 STM32H7 微控制器的音频采集与处理系统,能够采集音频信号,并将其以 WAV 格式保存至 SD 卡。采用 LED 闪烁来为用户提供各种状态反馈,包括录音状态、错误信息等。 二、项目模块结构 (一)错误处理与 LED 显示模块 功能:统一处理系统中的各类错误情况,并通过控制 LED 的不同闪烁模式向用户反馈相应信息。同时,也用于设置正常录音状态下的 LED 显示模式。 代码实现: // 定义不同的闪烁模式 #define LED_OFF 0 #define LED_ON 1 #define LED_BLINK_FAST 2 #define LED_BLINK_SLOW 3 #define LED_ALTERNATE_BLINK 4 // 定义错误码对应的闪烁模式 #define ERROR_MOUNT_SD_CARD LED_BLINK_FAST #define ERROR_OPEN_FILE LED_BLINK_FAST #define ERROR_ADC_DMA_INIT LED_BLINK_FAST #define RECORDING_ON LED_ON #define RECORDING_OFF LED_OFF #define ERROR_LONG_PRESS LED_BLINK_FAST // 错误处理函数,通过LED闪烁显示错误 void handleError(uint8_t errorMode) { switch (errorMode) { case ERROR_MOUNT_SD_CARD: case ERROR_OPEN_FILE: case ERROR_ADC_DMA_INIT: case ERROR_LONG_PRESS: // 设置LED闪烁模式来表示错误 setLEDMode(errorMode); break; default: break; } } // 设置LED的闪烁模式 void setLEDMode(uint8_t mode) { switch (mode) { case LED_OFF: HAL_GPIO_WritePin(GPIO_PIN_1, GPIO_PIN_RESET); // LD1(绿色)关闭 HAL_GPIO_WritePin(GPIO_PIN_5, GPIO_PIN_RESET); // LD2(橙色)关闭 HAL_GPIO_WritePin(GPIO_PIN_2, GPIO_PIN_SET); // LD3(红色)关闭 break; case LED_ON: if (mode == RECORDING_ON) { HAL_GPIO_WritePin(GPIO_PIN_1, GPIO_PIN_SET); // LD1(绿色)打开,表示录音中 HAL_GPIO_WritePin(GPIO_PIN_5, GPIO_PIN_RESET); // LD2(橙色)关闭 HAL_GPIO_WritePin(GPIO_PIN_2, GPIO_PIN_SET); // LD3(红色)关闭 } break; case LED_BLINK_FAST: // 实现快速闪烁逻辑(使用LD2橙色LED) for (int i = 0; i < 10; i++) { HAL_GPIO_TogglePin(GPIO_PIN_5, GPIO_PIN_SET); HAL_Delay(100); } break; case LED_BLINK_SLOW: // 实现慢速闪烁逻辑(使用LD2橙色LED) for (int i = 0; i < 5; i++) { HAL_GPIO_TogglePin(GPIO_PIN_5, GPIO_PIN_SET); HAL_Delay(500); } break; case LED_ALTERNATE_BLINK: // 实现交替闪烁逻辑 for (int i = 0; i < 5; i++) { HAL_GPIO_TogglePin(GPIO_PIN_1, GPIO_PIN_SET); HAL_GPIO_TogglePin(GPIO_PIN_5, GPIO_PIN_SET); HAL_Delay(300); } break; } }   在这个模块中,handleError函数接收错误模式参数,并调用setLEDMode函数来控制 LED 的闪烁模式。不同的错误或状态对应不同的闪烁模式,如ERROR_MOUNT_SD_CARD等错误码对应LED_ALTERNATE_BLINK模式,而录音开启状态RECORDING_ON对应LED_ON模式,通过LED1(红灯)和LED2(绿灯)的亮灭或LED3(用于特殊状态显示)的闪烁来向用户传达信息。 (二)音频采集模块 功能:利用 STM32 的 ADC(轮询方式)启动音频信号采集过程。 代码实现: // 初始化音频采集 void InitAudioCapture() { if (HAL_ADC_Start(&hadc1)!= HAL_OK) { handleError(ERROR_ADC_START); } }   InitAudioCapture函数用于初始化音频采集功能。它主要负责启动 STM32 芯片上的模数转换器(ADC)。如果 ADC 启动成功,那么后续就可以开始采集音频信号;如果启动失败,则通过调用handleError函数来处理错误,这里的错误处理方式是根据预定义的错误模式(ERROR_ADC_START)来控制 LED 闪烁,向用户提示 ADC 启动出现问题。 (三)WAV 文件生成模块 功能:生成符合 WAV 格式标准的文件头。 代码实现: // 生成WAV文件头 void generateWavHeader(FIL *file) { uint32_t dataSize = 0; uint32_t byteRate = SAMPLE_RATE * NUM_CHANNELS * (BITS_PER_SAMPLE / 8); uint32_t blockAlign = NUM_CHANNELS * (BITS_PER_SAMPLE / 8); // RIFF 块 const char riffHeader[] = {'R', 'I', 'F', 'F'}; f_write(file, riffHeader, sizeof(riffHeader), NULL); f_write(file, &dataSize, sizeof(dataSize), NULL); const char waveFormat[] = {'W', 'A', 'V', 'E'}; f_write(file, waveFormat, sizeof(waveFormat), NULL); // fmt 子块 const char fmtHeader[] = {'f', 'm', 't', ' '}; f_write(file, fmtHeader, sizeof(fmtHeader), NULL); const uint32_t fmtChunkSize = 16; f_write(file, &fmtChunkSize, sizeof(fmtChunkSize), NULL); const uint16_t audioFormat = 1; f_write(file, &audioFormat, sizeof(audioFormat), NULL); f_write(file, &NUM_CHANNELS, sizeof(NUM_CHANNELS), NULL); f_write(file, &SAMPLE_RATE, sizeof(SAMPLE_RATE), NULL); f_write(file, &byteRate, sizeof(byteRate), NULL); f_write(file, &blockAlign, sizeof(blockAlign), NULL); f_write(file, &BITS_PER_SAMPLE, sizeof(BITS_PER_SAMPLE), NULL); // data 子块 const char dataHeader[] = {'d', 'a', 't', 'a'}; f_write(file, dataHeader, sizeof(dataHeader), NULL); f_write(file, &dataSize, sizeof(dataSize), NULL); }   该模块根据定义好的音频参数(如采样率SAMPLE_RATE、声道数NUM_CHANNELS、每个样本的位数BITS_PER_SAMPLE)生成 WAV 文件头。此函数在录音保存到 SD 卡的过程中被调用。 (四)录音保存到 SD 卡模块 功能:将采集到的音频数据保存为 WAV 格式文件到 SD 卡,并以日期时间命名文件,同时处理文件操作过程中的错误情况。 代码实现:   此模块首先尝试挂载 SD 卡文件系统,在这一过程中若挂载失败(即f_mount函数返回值不为FR_OK),则通过handleError函数以ERROR_MOUNT_SD_CARD对应的 LED 闪烁模式向用户提示错误信息。挂载成功后,接着会根据当前日期和时间来创建文件名。之后尝试打开此文件,若文件打开操作失败(也就是f_open函数返回值不为FR_OK),同样会调用handleError函数,同时卸载文件系统。当文件成功打开后,会生成符合 WAV 格式的文件头。在录音过程中,使用轮询 ADC 转换是否完成,完成后获取其值并将音频数据逐个写入文件。待录音结束后,根据已写入文件的音频数据量来更新 WAV 文件头中的数据大小信息,最后关闭文件,并卸载 SD 卡文件系统。 (五)按键控制模块 功能:实现按键防抖功能,并通过按键控制录音的开始、停止、暂停等操作,同时在出现异常按键情况(如长按)时通过 LED 显示错误信息。 代码实现: // 更完善的按键防抖和长按检测 #define DEBOUNCE_COUNT 5 #define LONG_PRESS_TIME 1000 // 长按时间阈值(单位:毫秒) uint8_t buttonDebounceCounter = 0; uint32_t buttonPressStartTime = 0; // 假设这里的 BUTTON_Pin 就是控制 B2 按键的引脚(PC13) #define BUTTON_Pin GPIO_PIN_13 #define BUTTON_PORT GPIOC uint8_t debounceButton(uint16_t GPIO_Pin) { uint8_t currentState = HAL_GPIO_ReadPin(BUTTON_PORT, BUTTON_Pin); // 读取B2按键引脚状态 if (currentState == button.previousState) { if (currentState == GPIO_PIN_SET) { // 按下为高电平 buttonDebounceCounter++; if (buttonDebounceCounter >= DEBOUNCE_COUNT) { if (HAL_GetTick() - buttonPressStartTime > LONG_PRESS_TIME) { handleError(ERROR_LONG_PRESS); return GPIO_PIN_SET; // 返回高电平表示长按 } } } else { buttonDebounceCounter = 0; buttonPressStartTime = HAL_GetTick(); } } else { buttonDebounceCounter = 0; button.previousState = currentState; } return 0; } // 按键中断处理 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == BUTTON_Pin && debounceButton(GPIO_Pin) == GPIO_PIN_SET) { // 检测到B2按下 switch (buttonAction) { case BUTTON_PRESS_SHORT: recording =!recording; setLEDMode(recording? RECORDING_ON : RECORDING_OFF); if (recording) { SaveAudioToSDCard(); } break; default: break; } } }   在debounceButton函数中,实现了按键防抖和长按检测功能。当检测到按键按下且经过防抖处理后,如果按键按下时间超过LONG_PRESS_TIME阈值,判定为长按操作,调用handleError函数以ERROR_LONG_PRESS对应的 LED 闪烁模式提示用户。在HAL_GPIO_EXTI_Callback函数中,处理短按按键操作,根据按键状态切换录音状态,并通过setLEDMode函数设置相应的 LED 显示模式(录音开启时为RECORDING_ON模式,录音停止时为RECORDING_OFF模式),同时在录音开始时调用SaveAudioToSDCard函数开始保存音频。   实际操作时候,保存文件一直有问题,保存的文件打开音频有些失真,应该是采样率的问题,还需要修改调试一下。。。

  • 2024-11-07
  • 回复了主题帖: # STM32H7S78-DK 开发套件三周目评测:简单声音采集保存之使用 SD 卡读写的实现与分析

    cc1989summer 发表于 2024-11-2 13:03 哥们,你这帖子在那干讲,没开发板跑的实物图啊。 后面加,哈哈哈

  • 2024-11-01
  • 回复了主题帖: 《STM32H7S78-DK 开发套件二周目评测:简单声音采集之驱动MAX4466高精度声音传感器》

    cc1989summer 发表于 2024-10-30 21:46 哥们,翻到开发板背面,靠近USB1上面,有个芯片U1:MP23DB01,就是数字麦克风 o.o!!!我看手册是写有麦克风,但是又看示意图写的是一个麦克风接口。原来是这样!

  • 2024-10-31
  • 发表了主题帖: # STM32H7S78-DK 开发套件三周目评测:简单声音采集保存之使用 SD 卡读写的实现与分析

    该项目包括了保存录音、按键控制、串口控制、频率成分分析与分类等功能,逐步进行完成。 首先进行sd读写的开。在嵌入式开发中,尤其在无操作系统的裸机环境下,SD卡是实现大容量数据存储的重要组件。 ## 1. 硬件连接 在开始CubeMX配置前,需要先了解引脚的配置。 ### 1.1 数据引脚连接 - **D0 - D3**:将SD卡的数据引脚D0-D3分别连接到STM32的PC8、PC9、PC10、PC11。 - **CMD**:将SD卡的CMD引脚连接到STM32的PD2,用于命令传输。 - **CLK**:将SD卡的CLK引脚连接到STM32的PC12,提供时钟信号。 - **SD_Detect**:将SD卡的SD_Detect引脚连接至GPIO(PM14),用于检测SD卡插入状态。 具体连接示例代码: ```c // 配置引脚,在CubeMX中生成的初始化代码中可见 void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; // Enable clock for GPIOC and GPIOD __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOM_CLK_ENABLE(); // 使能PM14引脚所在的GPIO时钟 // 配置SDMMC引脚:CMD (PD2), CLK (PC12), D0 (PC8), D1 (PC9), D2 (PC10), D3 (PC11) GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_2; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF12_SDMMC1; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); // 配置SD_Detect引脚 (PM14) GPIO_InitStruct.Pin = GPIO_PIN_14; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉电阻 HAL_GPIO_Init(GPIOM, &GPIO_InitStruct); } ``` ## 2. CubeMX 配置与代码实现 CubeMX能够简化SD卡和文件系统的配置,我们将通过它来生成SD卡的初始化代码,并使用FATFS文件系统以实现文件操作。这里选择FATFS文件系统方式除了是可以直接配置简单快捷外,使用eprom还有一些缺点: 1.没有文件系统,sd卡在电脑等设备无法读取。 2.读写速度慢、读写区域小。 ### 2.1 CubeMX 配置步骤 #### 2.1.1 SDMMC 接口设置 1. 在CubeMX中打开**Peripherals -> SDMMC1**接口。 2. 将其模式设置为**4-bit**模式,以提升数据传输速率。 3. 在**Clock Configuration**中将SDMMC1的时钟频率调整为48MHz,以保证传输速率和稳定性。 #### 2.1.2 启用 FATFS 文件系统 1. 在**Middlewares**中启用FATFS支持,选择SDMMC1作为接口,以实现基于文件系统的操作。 2. 生成初始化代码,其中`fatfs.c`文件包含了文件系统初始化和读写的基本接口。 ### 2.2 基本代码示例 接下来使用FATFS文件系统在SD卡上进行文件创建、写入和读取: ```c #include "fatfs.h" #include "sdmmc.h" #include void SD_Test() { FATFS fs; FIL file; FRESULT res; UINT bw; // 挂载文件系统 res = f_mount(&fs, "0:", 1); if (res == FR_OK) { // 创建并写入文件 res = f_open(&file, "0:/test.txt", FA_CREATE_ALWAYS | FA_WRITE); if (res == FR_OK) { char writeData[] = "Hello, SD Card!"; f_write(&file, writeData, sizeof(writeData), &bw); f_close(&file); } // 读取文件 res = f_open(&file, "0:/test.txt", FA_READ); if (res == FR_OK) { char readData[32]; f_read(&file, readData, sizeof(readData), &bw); printf("Read data: %s\n", readData); f_close(&file); } // 卸载文件系统 f_mount(NULL, "0:", 1); } } ``` ## 3. 不同 SD 卡读写方式与代码分析 当然在不同场景和需求下,对sd卡的读写操作应该有适当改变,才能提高效率、节省资源。 ### 3.1 基于文件系统的标准读写 - **优点**:CubeMX自动生成FATFS代码,简化了文件操作流程,适用于低频存储。比如传感器的数据记录等。 - **缺点**:由于文件系统需要索引和管理结构,写入速度会受影响。 ```c void SD_WriteRead_FileSystem() { FATFS fs; FIL file; UINT bw, br; char writeData[] = "File System Write Test"; char readData[32]; // 挂载文件系统 f_mount(&fs, "0:", 1); // 写入文件 f_open(&file, "0:/filesystem.txt", FA_CREATE_ALWAYS | FA_WRITE); f_write(&file, writeData, sizeof(writeData), &bw); f_close(&file); // 读取文件 f_open(&file, "0:/filesystem.txt", FA_READ); f_read(&file, readData, sizeof(readData), &br); f_close(&file); printf("Read Data: %s\n", readData); f_mount(NULL, "0:", 1); } ``` ### 3.2 扇区级块读写 - **优点**:直接操作扇区避免了文件系统开销,适合高频数据写入。比如音视频、网络文件等。 - **缺点**:需要手动管理数据位置和扇区分配,编程难度大。 ```c void SD_WriteBlock(uint32_t sector, uint8_t *data, uint32_t count) { if (HAL_SD_WriteBlocks(&hsd1, data, sector, count, HAL_MAX_DELAY) == HAL_OK) { printf("Block write success!\n"); } } void SD_ReadBlock(uint32_t sector, uint8_t *data, uint32_t count) { if (HAL_SD_ReadBlocks(&hsd1, data, sector, count, HAL_MAX_DELAY) == HAL_OK) { printf("Block read success!\n"); } } ``` ### 3.3 缓存 + 文件系统混合模式 - **优点**:使用缓存减少频繁写入,提升写入效率,适合音频等连续数据存储。 - **缺点**:占用较多RAM资源,需考虑系统内存限制。 ```c #define BUFFER_SIZE 512 uint8_t cacheBuffer[BUFFER_SIZE]; void SD_WriteCache(const char *data) { static uint32_t offset = 0; while (*data) { cacheBuffer[offset++] = *data++; if (offset >= BUFFER_SIZE) { SD_WriteBlock(0, cacheBuffer, BUFFER_SIZE / 512); // 以块为单位写入 offset = 0; } } } ``` ## 4. 音频数据存储的优化方案 考虑到录音是一个时效性很强的操作,所以对存储效率要求很高,下面是两个优化的方案。 ### 4.1 双缓冲技术 设置两个缓冲区,交替写入SD卡以减小写入延时。 ```c uint8_t bufferA[BUFFER_SIZE]; uint8_t bufferB[BUFFER_SIZE]; volatile uint8_t *currentBuffer = bufferA; void Audio_WriteToSD() { if (currentBuffer == bufferA) { SD_WriteBlock(0, bufferA, BUFFER_SIZE / 512); currentBuffer = bufferB; } else { SD_WriteBlock(0, bufferB, BUFFER_SIZE / 512); currentBuffer = bufferA; } } ``` ### 4.2 文件分段存储 音频数据按时间或大小分段存储,避免大文件操作带来的开销。 ```c void SaveAudioSegment(uint8_t *data, size_t size, uint8_t segmentNum) { char filename[20]; sprintf(filename, " audio_segment_%d.wav", segmentNum); f_open(&file, filename, FA_CREATE_ALWAYS | FA_WRITE); f_write(&file, data, size, &bw); f_close(&file); } ```

  • 2024-10-30
  • 回复了主题帖: 《STM32H7S78-DK 开发套件二周目评测:简单声音采集之驱动MAX4466高精度声音传感器》

    cc1989summer 发表于 2024-10-29 00:31 楼主,STM32H7S78-DK本身就带数字麦克风,理论上可以通过该麦克风实现音量检测…… 我开始也是看他有数字麦克风,才想测评弄这个,但是我只找到一个麦克风接口,有点疑惑。所以就干脆直接买了一个麦克风

  • 2024-10-21
  • 回复了主题帖: 《STM32H7S78-DK 开发套件二周目评测:简单声音采集之频率检测与显示》

    Jacktang 发表于 2024-10-20 08:50 这个对检测的音频信号有没有要求呢,比如最低的频率大小   按照目前的采样率的话,理论上的最低频率大约为1hz,最高大约是500hz的样子

  • 2024-10-18
  • 发表了日志: 《STM32H7S78-DK 开发套件二周目评测:简单声音采集之频率检测与显示》

  • 发表了主题帖: 《STM32H7S78-DK 开发套件二周目评测:简单声音采集之频率检测与显示》

    在实现adc基础上,实现对音频信号频率的检测。读取模拟信号并通过 FFT(快速傅里叶变换)分析频率成分。同时,在串口输出检测到的频率,并通过 LED 显示结果。 一、硬件连接 引脚连接: 将 MAX4466 传感器的输出引脚连接到 STM32 的 ADC 输入引脚(PC0,即 ADC1_IN10)。 LED选取PO5。在高电平时候点亮。   二、使用 CubeMX 进行配置 在 CubeMX 中进行如下设置: 2.1 ADC 模块配置 时钟设置:配置 ADC 时钟为 84MHz,选择 ADC 的时钟分频器为 1,以达到较高的采样精度。 采样时间:设置 ADC 的采样时间为 15.5 个 ADC 时钟周期,以提高采样精度。 通道设置:将 ADC 通道设置为 ADC1_IN10(PC0),确保能够读取传感器输出。 2.2 TIM 模块配置 定时器配置:配置 TIM2 为定时器,用于生成定时中断。 设置预分频器为 7999,自动重装载值为 999,以设定频率采样率(例如 1kHz)。 2.3 GPIO 设置 将 PO5 配置为推挽输出模式,用于控制 LED。 生成代码并导入 Keil,在 CubeMX 中生成代码,并导入 Keil 进行后续的代码开发。 四、代码实现 4.1 初始化模块 在 Keil 中,初始化 ADC、定时器和 LED 控制的代码如下: #include "arm_math.h" #include "stm32h7xx_hal.h" #define FFT_SIZE 1024 // FFT 输入数组的大小 float32_t input[FFT_SIZE]; float32_t output[FFT_SIZE]; arm_cfft_instance_f32 fft_instance; TIM_HandleTypeDef htim2; ADC_HandleTypeDef hadc1; void ADC_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; __HAL_RCC_ADC12_CLK_ENABLE(); // 使能 ADC 时钟 hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV2; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = DISABLE; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; HAL_ADC_Init(&hadc1); sConfig.Channel = ADC_CHANNEL_10; // 选择通道 10 sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES; // 设置采样时间 HAL_ADC_ConfigChannel(&hadc1, &sConfig); } uint32_t Read_ADC_Value(void) { uint32_t adc_value = 0; HAL_ADC_Start(&hadc1); // 启动 ADC HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); // 等待转换完成 adc_value = HAL_ADC_GetValue(&hadc1); // 获取 ADC 值 HAL_ADC_Stop(&hadc1); // 停止 ADC return adc_value; } void TIM_Init(void) { __HAL_RCC_TIM2_CLK_ENABLE(); // 使能 TIM2 时钟 htim2.Instance = TIM2; htim2.Init.Prescaler = 7999; // 对应 1kHz htim2.Init.Period = 999; // 对应 1ms htim2.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数 HAL_TIM_Base_Init(&htim2); // 初始化定时器 HAL_TIM_Base_Start_IT(&htim2); // 启动定时器中断 } void FFT_Init(void) { arm_cfft_init_f32(&fft_instance, FFT_SIZE); // 初始化 FFT 实例 }   4.2 FFT 处理 使用 CMSIS DSP 库实现 FFT 分析信号频率: void Perform_FFT(void) { arm_cfft_f32(&fft_instance, input, 0, 1); // 执行 FFT arm_cmplx_mag_f32(input, output, FFT_SIZE); // 计算幅度 }   4.3 频率检测与 LED 显示 计算频率并控制 LED 的开关状态的代码如下: void Display_Frequency(void) { float max_value = output[0]; uint32_t max_index = 0; for (uint32_t i = 1; i < FFT_SIZE / 2; i++) { if (output[i] > max_value) { max_value = output[i]; max_index = i; } }   // 控制 LED 的开关状态 if (max_index > 10) { HAL_GPIO_WritePin(GPIOO, GPIO_PIN_5, GPIO_PIN_SET); // 点亮 LED } else { HAL_GPIO_WritePin(GPIOO, GPIO_PIN_5, GPIO_PIN_RESET); // 熄灭 LED } }   4.4 主程序 在主循环中进行 ADC 采集、FFT 处理和 LED 控制的代码如下: int main(void) { HAL_Init(); SystemClock_Config(); ADC_Init(); TIM_Init(); FFT_Init(); while (1) { UART_Send_Data(corrected_value); } } // 定时器溢出回调 void HAL_TIM_PERIOD_ELAPSED_CALLBACK(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { // 每次定时器溢出时读取 ADC 值 uint32_t adc_value = Read_ADC_Value(); // 获取 ADC 值 static uint32_t sample_index = 0; input[sample_index] = (float32_t)adc_value; // 存储到 FFT 输入数组 sample_index++; // 如果达到 FFT_SIZE 大小,则进行 FFT 计算 if (sample_index >= FFT_SIZE) { sample_index = 0; // 重置索引 Perform_FFT(); Display_Frequency(); } } }

  • 发表了主题帖: 《STM32H7S78-DK 开发套件二周目评测:简单声音采集之驱动MAX4466高精度声音传感器》

    本帖最后由 ccccccc@ 于 2024-10-18 11:52 编辑 MAX4466是一款高精度声音传感器,能够将声音信号转化为相应的模拟电压输出。STM32H7系列微控制器具备高性能的ADC(模数转换器)功能,可以精确地采集模拟信号并进行数字化处理。     使用STM32H7S78-DK开发板,通过ADC(模数转换器)采集MAX4466高精度声音传感器的模拟信号,最终实现一个简单的音量检测功能。 有下面个步骤: - 从MAX4466传感器采集音频信号。 - 对采集的数据进行处理,获得稳定的音量信息。   硬件连接: 引脚连接: 如上图所示,可以将MAX4466传感器的输出引脚连接到STM32的ADC输入引脚(即PC0,ADC12_IN10),设置该GPIO的ADC功能属性。   使用CubeMX进行配置: 在CubeMX中,进行如下设置: 选择ADC模块:选择ADC1,设置为12位分辨率,适合音频信号处理。 设置采样时间:设置为3.5个ADC时钟周期,确保稳定采样。 设置转换模式:选择单次转换模式(Single Conversion),适合简单应用。 时钟设置: 确保ADC时钟频率与采样时间相匹配,避免采样延迟。 引脚设置:配置PC0为ADC输入模式。 生成代码并导入Keil:  在CubeMX中生成代码,并导入Keil进行后续的代码开发。 在Keil中,检查  初始化ADC并读取值的代码 #include "stm32h7xx_hal.h" ADC_HandleTypeDef hadc1; void ADC_Init(void) { // 使能ADC时钟 __HAL_RCC_ADC12_CLK_ENABLE(); // ADC配置 hadc1.Instance = ADC1; hadc1.Init.Resolution = ADC_RESOLUTION_12B; // 12位分辨率 hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; // 禁用扫描模式 hadc1.Init.ContinuousConvMode = DISABLE; // 禁用连续转换 hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发 hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 数据右对齐 hadc1.Init.NbrOfConversion = 1; // 单次转换 HAL_ADC_Init(&hadc1); // 初始化ADC } uint32_t Read_ADC_Value(void) { HAL_ADC_Start(&hadc1); // 启动ADC转换 HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); // 等待转换完成 return HAL_ADC_GetValue(&hadc1); // 返回ADC值 } 数据处理:在main代码中进行添加下面的代码进行采集信息的数据处理 滤波处理: 使用简单的移动平均滤波器平滑采样值,减少波动。 float moving_average(float new_value) { static float avg = 0; static int count = 0; count++; avg += (new_value - avg) / count; // 计算平均值 return avg; } 偏移量校正:在初始化时获取基准值,以确保测量的准确性。 static int32_t offset = 0; void Calculate_Offset(void) { for (int i = 0; i < 100; i++) { HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); offset += HAL_ADC_GetValue(&hadc1); // 累加ADC值 } offset /= 100; // 计算平均偏移量 } uint32_t Get_Corrected_ADC_Value(void) { uint32_t adc_value = Read_ADC_Value(); // 读取ADC值 return adc_value - offset; // 返回校正后的值 }   UART配置: 通过UART输出采样结果,方便观察和调试。当然也可以使用LED进行对应的显示。在CUBEMAX进行对应的配置;       UART_HandleTypeDef huart1; void UART_Init(void) { // 使能USART1时钟 __HAL_RCC_USART1_CLK_ENABLE(); // USART配置 huart1.Instance = USART1; huart1.Init.BaudRate = 115200; // 波特率 huart1.Init.WordLength = USART_WORDLENGTH_8B; // 字长 huart1.Init.StopBits = USART_STOPBITS_1; // 停止位 huart1.Init.Parity = USART_PARITY_NONE; // 无奇偶校验 huart1.Init.Mode = USART_MODE_TX_RX; // 发送接收模式 HAL_UART_Init(&huart1); // 初始化UART } void UART_Send_Data(uint32_t data) { char buffer[20]; sprintf(buffer, "ADC Value: %lu\n", data); // 格式化输出 HAL_UART_Transmit(&huart1, (uint8_t*)buffer, strlen(buffer), HAL_MAX_DELAY); // 发送数据 } 7.主程序: 在主循环中读取ADC值,并通过UART输出。 int main(void) { HAL_Init(); // 初始化HAL库 ADC_Init(); // 初始化ADC UART_Init(); // 初始化UART Calculate_Offset(); // 计算偏移量 while (1) { uint32_t corrected_value = Get_Corrected_ADC_Value(); // 获取校正后的ADC值 float smoothed_value = moving_average(corrected_value); // 平滑处理 UART_Send_Data(corrected_value); // 发送ADC值 } }   8.关于噪声处理: 滤波策略:可以通过选择适当的滤波窗口,达到避免数据波动的效果。并且考虑使用中位数滤波或加权平均滤波,以处理异常值和噪声。   9.当然还可以进一步进行性能优化: 使用DMA: 对于快速连续采样的需求,可以考虑使用DMA(直接存储器访问)进行数据采集,减轻CPU负担。 DMA_HandleTypeDef hdma_adc1; void DMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); // 使能DMA时钟 hdma_adc1.Instance = DMA2_Stream0; hdma_adc1.Init.Channel = DMA_CHANNEL_0; // 选择通道 hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; // 从外设到内存 hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; // 禁用外设地址递增 hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; // 启用内存地址递增 hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; // 外设数据对齐 hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; // 内存数据对齐 hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式 hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; // 高优先级 HAL_DMA_Init(&hdma_adc1); // 初始化DMA __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); // 连接DMA与ADC }      

最近访客

< 1/3 >

统计信息

已有78人来访过

  • 芯积分:178
  • 好友:1
  • 主题:25
  • 回复:30

留言

你需要登录后才可以留言 登录 | 注册


现在还没有留言