2026/4/2 23:25:09
网站建设
项目流程
建设领域工人管理网站,成都环境建设网站,网站建设的市场定位,平陆网站建设1. 认识nRF52840的SPIM3外设
nRF52840作为Nordic Semiconductor的旗舰级蓝牙SoC#xff0c;其外设资源相当丰富。在SPI接口方面#xff0c;它提供了4个独立的SPIM#xff08;SPI Master#xff09;控制器#xff0c;其中SPIM3是性能最强劲的一个。这里有个有趣的发现其外设资源相当丰富。在SPI接口方面它提供了4个独立的SPIMSPI Master控制器其中SPIM3是性能最强劲的一个。这里有个有趣的发现前三个SPIM控制器SPIM0/1/2最高只能跑到8MHz而SPIM3却能飙到32MHz相当于前者的4倍速度不过天下没有免费的午餐SPIM3有两个特殊限制首先它只能工作在主机模式不能作为从机其次它必须配合EasyDMA使用。我在第一次使用时就被这个坑绊倒了当时还纳闷为什么配置好的从机模式就是不响应。后来仔细看数据手册才发现这个限制所以特别提醒大家注意。说到时钟频率的选择nRF52840的SPIM3支持以下速率125kHz250kHz500kHz1MHz2MHz4MHz8MHz16MHz32MHz实际项目中我测试过在32MHz下连续传输1MB数据相比8MHz的SPIM确实能节省不少时间。但要注意高速传输对PCB布线要求更高如果发现数据出错可能需要降低频率调试。2. 开发环境搭建要点我习惯使用Keil MDK作为开发环境当前用的是V5.28版本。协议栈方面nRF5_SDK_15.3.0_59ac345是个不错的选择当然其他版本也基本兼容。这里分享几个配置时容易忽略的细节首先在sdk_config.h中除了启用NRFX_SPIM_ENABLED外必须同时启用SPI_ENABLED和至少一个SPI实例建议选SPI2。这是因为SPIM3的驱动依赖这些基础配置。如果找不到这些选项可以从SDK示例工程里复制比如nRF5_SDK_15.3.0_59ac345\examples\peripheral\spi_master_using_nrf_spi_mngr\pca10056\blank\config\sdk_config.h接着需要将驱动文件nrfx_spim.c添加到工程中路径为SDK\modules\nrfx\drivers\src\nrfx_spim.c在Keil的Options for Target C/C Include Paths中添加头文件路径...\SDK\modules\nrfx\drivers\include有个硬件冲突需要注意TWII2C和SPI会共用外设资源。具体来说TWI0与SPI0不能同时使用TWI1与SPI1不能同时使用 所以在规划硬件设计时就要考虑好外设分配。3. SPIM3的硬件配置详解配置SPIM3需要定义几个关键数据结构。先来看驱动实例的定义static nrfx_spim_t driver_spi NRFX_SPIM_INSTANCE(3); // 使用SPIM3实例然后是发送和接收缓冲区。由于要用EasyDMA缓冲区必须放在RAM中static uint8_t driver_spi_tx_buf[6]; // 发送缓冲区 static uint8_t driver_spi_rx_buf[1]; // 接收缓冲区最核心的是传输配置结构体这里我拆解说明每个参数const static nrfx_spim_config_t driver_spi_config { .sck_pin NRF_GPIO_PIN_MAP(1, 9), // SCK引脚 P1.09 .mosi_pin NRF_GPIO_PIN_MAP(0, 12), // MOSI引脚 P0.12 .miso_pin NRF_GPIO_PIN_MAP(0, 7), // MISO引脚 P0.07 .ss_pin NRFX_SPIM_PIN_NOT_USED, // 手动控制CS .ss_active_high false, // CS低电平有效 .irq_priority NRFX_SPIM_DEFAULT_CONFIG_IRQ_PRIORITY, .orc 0xFF, // 溢出时发送的值 .frequency NRF_SPIM_FREQ_32M, // 32MHz时钟 .mode NRF_SPIM_MODE_0, // CPOL0, CPHA0 .bit_order NRF_SPIM_BIT_ORDER_MSB_FIRST // 高位在前 };初始化时建议使用阻塞模式简单可靠APP_ERROR_CHECK(nrfx_spim_init(driver_spi, driver_spi_config, NULL, NULL));实际项目中我发现GPIO的驱动能力会影响信号质量。如果传输距离较长比如超过10cm可以在配置中加入以下设置增强驱动nrf_gpio_cfg(driver_spi_config.sck_pin, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_DISCONNECT, NRF_GPIO_PIN_PULLDOWN, NRF_GPIO_PIN_H0H1, // 高驱动能力 NRF_GPIO_PIN_NOSENSE);4. 突破EasyDMA的255字节限制EasyDMA是nRF52840的特色功能可以自动搬运数据减轻CPU负担。但它有个烦人的限制单次传输不能超过255字节。经过多次尝试我总结出两种解决方案方案一分块传输void SPI_write(uint8_t *pBuffer, uint32_t size) { while(size 0) { uint8_t chunk (size 255) ? 255 : size; driver_spim_xfer.tx_length chunk; driver_spim_xfer.p_tx_buffer pBuffer; driver_spim_xfer.rx_length 0; driver_spim_xfer.p_rx_buffer NULL; APP_ERROR_CHECK(nrfx_spim_xfer(driver_spi, driver_spim_xfer, 0)); pBuffer chunk; size - chunk; } }方案二链式DMA更高级的做法是利用SPIM的LIST功能可以预先设置好多个DMA描述符。这种方式效率更高但配置也更复杂nrfx_spim_xfer_desc_t xfer_list[4]; // 初始化各个描述符... APP_ERROR_CHECK(nrfx_spim_xfer(driver_spi, xfer_list, 4));实测下来传输1MB数据时分块方案耗时约320ms而链式DMA可以缩短到290ms左右。如果对性能要求极高后者是更好的选择。5. 实战中的性能优化技巧要让SPIM3稳定跑在32MHz还需要注意以下几点PCB布局建议SCK走线尽可能短最好控制在50mm以内MOSI/MISO走线长度差不超过10mm在信号线旁布置地线作为回流路径避免信号线穿过电源分割区域软件优化技巧启用DC/DC转换器降低电源噪声在传输前调用__DSB()指令保证内存写入完成使用nrf_delay_us(1)在连续传输间插入微小延迟将SPI中断优先级设为最高0级时钟配置示例// 启用高频外部时钟 NRF_CLOCK-TASKS_HFCLKSTART 1; while(NRF_CLOCK-EVENTS_HFCLKSTARTED 0);如果发现数据错误可以尝试以下调试步骤先用1MHz低速测试检查电源纹波应50mV用逻辑分析仪抓取波形在SCK上拉22pF电容减小振铃6. 常见问题与解决方案问题一SPIM3无法工作可能原因未启用高频时钟需检查HFCLKSTARTED事件电源模式配置错误需设置VDDH3.3VGPIO配置冲突检查PIN_CNF寄存器问题二高速传输数据错误解决方法降低时钟频率测试检查PCB阻抗匹配增加SCK上升时间配置drive为S0S1在MISO上加10k上拉电阻问题三DMA传输卡死排查步骤检查缓冲区是否4字节对齐确认未访问未初始化的DMA通道查看SPIM-EVENTS_ENDTX事件是否触发检查电源管理是否意外进入低功耗模式有个特别隐蔽的坑当使用32MHz时钟时SPIM3的RX缓冲区最后一个字节可能会重复前一个字节的值。这是芯片的一个已知问题Errata 189解决方法是在读取后手动丢弃最后一个字节或者在缓冲区末尾额外多留一个空字节。7. 完整示例代码下面是我在实际项目中验证过的完整代码框架包含初始化、数据传输和错误处理#include nrfx_spim.h #include nrf_gpio.h #define SPI_SCK_PIN NRF_GPIO_PIN_MAP(1, 9) #define SPI_MOSI_PIN NRF_GPIO_PIN_MAP(0, 12) #define SPI_MISO_PIN NRF_GPIO_PIN_MAP(0, 7) #define SPI_CS_PIN NRF_GPIO_PIN_MAP(0, 11) static nrfx_spim_t spim NRFX_SPIM_INSTANCE(3); static uint8_t m_tx_buf[256]; static uint8_t m_rx_buf[256]; void spi_init(void) { nrfx_spim_config_t config { .sck_pin SPI_SCK_PIN, .mosi_pin SPI_MOSI_PIN, .miso_pin SPI_MISO_PIN, .ss_pin NRFX_SPIM_PIN_NOT_USED, .frequency NRF_SPIM_FREQ_32M, .mode NRF_SPIM_MODE_0, .bit_order NRF_SPIM_BIT_ORDER_MSB_FIRST, }; APP_ERROR_CHECK(nrfx_spim_init(spim, config, NULL, NULL)); // 增强GPIO驱动能力 nrf_gpio_cfg(SPI_SCK_PIN, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_DISCONNECT, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE); } void spi_transfer(uint8_t *tx_data, uint8_t *rx_data, uint32_t length) { nrfx_spim_xfer_desc_t xfer { .p_tx_buffer tx_data, .tx_length length, .p_rx_buffer rx_data, .rx_length length }; // 手动控制CS nrf_gpio_pin_clear(SPI_CS_PIN); APP_ERROR_CHECK(nrfx_spim_xfer(spim, xfer, 0)); nrf_gpio_pin_set(SPI_CS_PIN); } void spi_large_transfer(uint8_t *data, uint32_t length) { while(length 0) { uint32_t chunk (length 255) ? 255 : length; spi_transfer(data, NULL, chunk); data chunk; length - chunk; } }这个框架已经成功应用在多个高速数据采集项目中包括传感器数据读取和显示屏刷新等场景。关键是要根据实际硬件调整GPIO配置和时序参数。