arduino - Arduino 未注册来自 STM32F407 的 SPI 传输
问题描述
编辑/解决:问题最终是我在 Arduino 代码中的 Serial.print 语句,这些语句减慢了实际通信代码的执行速度。基本上,Arduino 在 STM 板完成发送后接收字节,所以它只注册 'd'/100。
我目前正在参加在线课程,但其中一个练习存在问题。这个练习看起来很简单,但我似乎无法让一切正常工作。练习是从主控 MCU STM32F407 向 Arduino Uno 板发送一个字符串,然后应该打印消息及其长度。
Arduino 板似乎注册了 SS,但不会打印任何内容、打印不完整/不正确的消息(例如“hello vorl”)或打印乱码。经过一些调试后,我发现我错误地设置了我的 CPOL 和 CPHA 寄存器,但是,现在它不会打印任何接近“Hello world”的内容。经过更多调试,似乎 Arduino 卡在了它的 SPI_SlaveReceive 函数中。STM 的 SPI 代码设计为首先发送消息的长度,然后发送实际的消息。据我所知,Arduino 似乎没有为第一次传输注册正确的长度,这弄乱了应该获取实际消息的 SPI_SlaveReceive 调用的循环。
我是嵌入式新手,并且一直在尝试自己学习。我觉得我可能缺少一些基本的东西,但不知道。我也觉得代码设计得很糟糕,但我试图坚持课程(STM代码是按照指南“编写”的,而arduino代码只是提供了)。我将在下面发布代码,但我什至会欣赏关于可能出错/如何调试的想法。在所有内容下方,我还将发布一些错误记录。
这也是我的第一篇 stackoverflow 帖子,所以如果我需要添加(或删除)任何内容,请告诉我。谢谢!
更新:我认为这可能与时钟速度有关。我尝试在调试器中再次运行代码,发现如果我在修改 SPI 的数据寄存器的代码上放置一个断点,我就能够成功发送数据。看起来 SPI 发送数据的速度可能比 Arduino 可以处理的更快。如果我单步执行发送消息“Hello world”长度的代码,那么我可以看到 Arduino 成功接收到 11。如果我继续单步执行 pSPIx->DR = *pTxBuffer; 第二次调用SPI_SendData的指令,我可以成功发送消息。但是,如果在发送长度后,我会尝试一步完成对 SPI_SendData 的第二次调用。arduino 没有注册整个字符串。我尝试将 SPI 预标量修改为更大的值,这似乎有帮助。ie arduino 能够输出更多有效字符(导致“hellod”)。然而,这让我感到困惑,因为控制 SPI2 的 APB1 的最大频率为 42MHz。我发现给我最好的结果的预标量值是 7,这意味着 PCLK 除以 256,如果我正确地考虑这一点,最多可以得到 164 MHz 的频率。根据 Ardunio 的规格,处理此通信应该没有问题。我想我可能有一个问题是比特的发送频率是多少?也许我的 SPI_SendData 函数不能正常工作?然而,这让我感到困惑,因为控制 SPI2 的 APB1 的最大频率为 42MHz。我发现给我最好的结果的预标量值是 7,这意味着 PCLK 除以 256,如果我正确地考虑这一点,最多可以得到 164 MHz 的频率。根据 Ardunio 的规格,处理此通信应该没有问题。我想我可能有一个问题是比特的发送频率是多少?也许我的 SPI_SendData 函数不能正常工作?然而,这让我感到困惑,因为控制 SPI2 的 APB1 的最大频率为 42MHz。我发现给我最好的结果的预标量值是 7,这意味着 PCLK 除以 256,如果我正确地考虑这一点,最多可以得到 164 MHz 的频率。根据 Ardunio 的规格,处理此通信应该没有问题。我想我可能有一个问题是比特的发送频率是多少?也许我的 SPI_SendData 函数不能正常工作?我想我可能有一个问题是比特的发送频率是多少?也许我的 SPI_SendData 函数不能正常工作?我想我可能有一个问题是比特的发送频率是多少?也许我的 SPI_SendData 函数不能正常工作?
其他详情:
- 连接板的 4 根线:GND、CLK (STM: PB13 <-> UNO: 13)、MOSI (STM: PB15 <-> A: 11) 和 NSS (STM: PB12 <-> A:10)
- 我没有逻辑分析仪,所以我无法看到实际发送的内容(这很糟糕)
- 我意识到如果不发布所有代码可能很难获得完整的上下文,但我认为这太多了,所以尝试只发布相关部分。
记录:
16:01:42.461 -> Slave Initialized
16:01:42.494 -> Slave waiting for ss to go low
16:01:45.500 -> slave selected
16:01:45.500 -> Waiting to receive length
16:01:45.534 -> Length: 68
16:01:47.520 -> Received:
16:01:47.554 -> Received: d
16:01:48.968 -> Received:
16:01:48.968 -> Received: d
16:01:49.858 -> Received:
16:01:49.858 -> Received: d
16:01:50.499 -> Received:
16:01:50.499 -> Received: d
16:01:51.137 -> Received:
16:01:51.137 -> Received: d
16:01:51.707 -> Received:
16:01:51.741 -> Received: d
16:01:52.244 -> Received:
16:01:52.277 -> Received: d
16:01:52.782 -> Received:
16:01:52.782 -> Received: d
16:01:53.360 -> Received:
16:01:53.393 -> Received: d
16:01:53.866 -> Received:
16:01:53.899 -> Received: d
16:01:54.441 -> Received:
16:01:54.441 -> Received: d
16:01:54.946 -> Received:
16:01:54.980 -> Received: d
16:01:55.484 -> Received:
16:01:55.518 -> Received: d
16:01:55.994 -> Received:
16:01:56.027 -> Received: d
16:01:56.599 -> Received:
16:01:56.599 -> Received: d
16:01:57.105 -> Received:
16:01:57.138 -> Received: `
16:01:57.609 -> Received:
16:01:57.644 -> Received: d
16:01:58.085 -> Received:
16:01:58.118 -> Received: d
16:01:58.622 -> Received:
16:01:58.622 -> Received: d
16:01:59.128 -> Received:
16:01:59.161 -> Received: d
16:01:59.676 -> Received:
16:01:59.676 -> Received: d
16:02:00.146 -> Received:
16:02:00.180 -> Received: d
16:02:00.654 -> Received:
16:02:00.654 -> Received: d
16:02:01.127 -> Received:
16:02:01.127 -> Received: d
16:02:01.634 -> Received:
16:02:01.668 -> Received: d
16:02:02.105 -> Received:
16:02:02.139 -> Received: d
16:02:02.582 -> Received:
16:02:02.582 -> Received: d
16:02:03.052 -> Received:
16:02:03.052 -> Received: d
16:02:03.729 -> Received:
16:02:03.729 -> Received: d
16:02:04.236 -> Received:
16:02:04.236 -> Received: d
16:02:04.774 -> Received:
16:02:04.774 -> Received: d
16:02:05.336 -> Received:
16:02:05.336 -> Received: d
16:02:05.947 -> Received:
16:02:05.980 -> Received: d
16:02:06.485 -> Received:
16:02:06.485 -> Received: d
16:02:06.518 -> done receiving word
16:02:06.518 -> Rcvd:
16:02:06.553 -> ddddddddddddddd`dddddddddddddddddd
16:02:06.620 -> Length:68
16:02:06.620 -> Slave deselected
16:02:06.654 -> Slave waiting for ss to go low
16:02:07.025 -> slave selected
16:02:07.058 -> Waiting to receive length
16:02:07.092 -> Length: 100
Arduino代码:
/* SPI Slave Demo
*
* SPI pin numbers:
* SCK 13 // Serial Clock.
* MISO 12 // Master In Slave Out.
* MOSI 11 // Master Out Slave In.
* SS 10 // Slave Select . Arduino SPI pins respond only if SS pulled low by the master
*
*/
#include <SPI.h>
#include<stdint.h>
#define SPI_SCK 13
#define SPI_MISO 12
#define SPI_MOSI 11
#define SPI_SS 10
char dataBuff[500];
//Initialize SPI slave.
void SPI_SlaveInit(void)
{
#if 0
// Initialize SPI pins.
pinMode(SPI_SCK, INPUT);
pinMode(SPI_MOSI, INPUT);
pinMode(SPI_MISO, OUTPUT);
pinMode(SPI_SS, INPUT);
// Enable SPI as slave.
SPCR = (1 << SPE);
#endif
// Initialize SPI pins.
pinMode(SCK, INPUT);
pinMode(MOSI, INPUT);
pinMode(MISO, OUTPUT);
pinMode(SS, INPUT);
//make SPI as slave
// Enable SPI as slave.
SPCR = (1 << SPE);
}
//This function returns SPDR Contents
uint8_t SPI_SlaveReceive(void)
{
/* Wait for reception complete */
while(!(SPSR & (1<<SPIF)));
/* Return Data Register */
return SPDR;
}
//sends one byte of data
void SPI_SlaveTransmit(char data)
{
/* Start transmission */
SPDR = data;
/* Wait for transmission complete */
while(!(SPSR & (1<<SPIF)));
}
// The setup() function runs right after reset.
void setup()
{
// Initialize serial communication
Serial.begin(9600);
// Initialize SPI Slave.
SPI_SlaveInit();
Serial.println("Slave Initialized");
}
uint16_t dataLen = 0;
uint32_t i = 0;
// The loop function runs continuously after setup().
void loop()
{
Serial.println("Slave waiting for ss to go low");
while(digitalRead(SS) );
Serial.println("slave selected");
// Serial.println("start");
//1. read the length
// dataLen = (uint16_t)( SPI_SlaveReceive() | (SPI_SlaveReceive() << 8) );
//Serial.println(String(dataLen,HEX));
i = 0;
Serial. println("Waiting to receive length");
dataLen = SPI_SlaveReceive();
for(i = 0 ; i < dataLen ; i++ )
{
dataBuff[i] = SPI_SlaveReceive();
Serial.print("Received: ");
Serial.println(dataBuff[i]);
}
Serial.println("done receiving word");
// Serial.println(String(i,HEX));
dataBuff[i] = '\0';
Serial.println("Rcvd:");
Serial.println(dataBuff);
Serial.print("Length:");
Serial.println(dataLen);
while(!digitalRead(SS));
Serial.println("Slave deselected");
}
主要STM代码:
#include <string.h>
#include "stm32f407xx.h"
/*
* PB15 --> SPI2_MOSI
* PB14 --> SPI2_MISO
* PB13 --> SPI2_SCLK
* PB12 --> SPI2_NSS
* ALT Function mode: 5
*/
void SPI_GPIO_Setup(void);
void GPIO_ButtonInit(void);
void SPI_Setup(void);
void delay(int time){
for(uint32_t i=0; i<time; i++);
}
int main(void) {
char userData[] = "Hello world";
//initizalize button
GPIO_ButtonInit();
//This function is used to initialize the gpio pins to behave as spi pins
SPI_GPIO_Setup();
//This function is used to initialize the SPI peripheral
SPI_Setup();
//this makes NSS signal internally high and avoids MODF error
//NOTE: This should be taken care of in my SPI_Init function
// SPI_SSIConfig(SPI2, ENABLE);
//SSOE to 1 does NSS output enable
//I don't want to enable this in this way, but it is more simple due to the code design
//i.e. (using shifts instead of masks for registers)
SPI_SSOEConfig(SPI2, ENABLE);
while(1){
//wait for button to be pressed
while(! GPIO_ReadFromInputPin(GPIOA, GPIO_PIN_0));
delay(250000); //used for button debouncing
//enable the SPI2 peripheral --- according to generated code, this seems to be done in the transmit function
// I a SET_BIT macro in the transmit function if SPI isn't enabled, make sure to test this
SPI_PeripheralControl(SPI2, ENABLE);
//first send the length of data (Arduino expects this as 1 byte)
uint8_t dataLen = strlen(userData);
SPI_SendData(SPI2, &dataLen, 1);
//send data
SPI_SendData(SPI2, (uint8_t*)userData, dataLen);
//confirm the SPI is not busy
while(SPI_GetFlagStatus(SPI2, SPI_BSY_FLAG));
//disable the SPI2 peripheral --- according to Generated code, this seems to be done in abort functions
SPI_PeripheralControl(SPI2, DISABLE);
}
return 0;
}
void SPI_GPIO_Setup(void) {
GPIO_Handle_t SPIpins;
SPIpins.pGPIOx = GPIOB;
SPIpins.GPIO_PinConfig.GPIO_PinMode = GPIO_MODE_ALTFN;
SPIpins.GPIO_PinConfig.GPIO_PinAltFunMode = 5;
SPIpins.GPIO_PinConfig.GPIO_PinOPType = GPIO_OPTYPE_PP;
SPIpins.GPIO_PinConfig.GPIO_PinPuPdControl = GPIO_PU;
SPIpins.GPIO_PinConfig.GPIO_PinSpeed = GPIO_SPEED_FAST;
//MOSI
SPIpins.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_15;
GPIO_Init(&SPIpins);
//MISO
// SPIpins.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_14;
// GPIO_Init(&SPIpins);
//SCLK
SPIpins.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_13;
GPIO_Init(&SPIpins);
//NSS
SPIpins.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_12;
GPIO_Init(&SPIpins);
}
void GPIO_ButtonInit(void){
GPIO_Handle_t GpioBtn;
// configure button
//Set up PA0 to be an input for the button press
GpioBtn.pGPIOx = GPIOA;
GpioBtn.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_0;
GpioBtn.GPIO_PinConfig.GPIO_PinMode = GPIO_MODE_IN;
GpioBtn.GPIO_PinConfig.GPIO_PinSpeed = GPIO_SPEED_FAST;
GpioBtn.GPIO_PinConfig.GPIO_PinPuPdControl = GPIO_NO_PUPD;
GPIO_Init(&GpioBtn);
}
void SPI_Setup(void){
SPI_Handle_t hSPI;
hSPI.SPIx = SPI2;
hSPI.SPIConfig.SPI_BusConfig = SPI_BUS_CONFIG_FD;
hSPI.SPIConfig.SPI_DeviceMode = SPI_DEVICE_MODE_MASTER;
hSPI.SPIConfig.SPI_SclkSpeed = SPI_SCLK_SPEED_DIV8; //generate SCLK of 2 MHz
hSPI.SPIConfig.SPI_DFF = SPI_DFF_8BITS;
hSPI.SPIConfig.SPI_CPOL = SPI_CPOL_LOW;
hSPI.SPIConfig.SPI_CPHA = SPI_CPHA_LOW;
hSPI.SPIConfig.SPI_SSM = SPI_SSM_DI; //Hardware slave management disabled for NSS pin
SPI_Init(&hSPI);
}
SPI驱动代码:
/*
* stm32f407xx_spi_driver.c
*
* Created on: Oct 17, 2019
* Author: pigmo
*/
#include "stm32f407xx_spi_driver.h"
/**************************************************************************
* APIs supported by this driver
* For more information about these APIs check the function description
**************************************************************************/
/*
* Peripheral clock setup
*/
/******************************************************
* @fn - SPI_PeriClockControl
*
* @brief - Enables or disables peripheral clock for the given SPI port
*
* @param[in] - base address of SPI peripheral
* @param[in] - ENABLE or DISABLE macros
*
* @return - none
*
* @Note - SPI4 is reserved and won't be affected by this function
*/
void SPI_PeriClockControl(SPI_RegDef_t* pSPIx, uint8_t enOrDi){
if(enOrDi == ENABLE){
if(pSPIx == SPI1){
SPI1_PCLK_EN();
} else if (pSPIx == SPI2){
SPI2_PCLK_EN();
} else if (pSPIx == SPI3){
SPI3_PCLK_EN();
}
} else {
if(pSPIx == SPI1){
SPI1_PCLK_DI();
} else if (pSPIx == SPI2){
SPI2_PCLK_DI();
} else if (pSPIx == SPI3){
SPI3_PCLK_DI();
}
}
}
/*
* Init and De-init
*/
/******************************************************
* @fn - SPI_Init
*
* @brief - Initializes a given GPIO peripheral with given configurations
*
* @param[in] - Struct holding base address and desired configurations of SPI peripheral
*
* @return - none
*
* @Note - I don't like the way this is done, but I'm doing it to be consistent with the course
* Instead, I think the SPIConfig should hold masks, not enum type values
*/
void SPI_Init(SPI_Handle_t *pSPIHandle){
//enable the spi clock
SPI_PeriClockControl(pSPIHandle->SPIx, ENABLE);
//disable the SPI (concept taken from STM generated code)
SPI_PeripheralControl(pSPIHandle->SPIx, DISABLE);
//first configure the SPI CR1 register
uint32_t tempReg = 0;
//1. configure the device mode
if(pSPIHandle->SPIConfig.SPI_DeviceMode == SPI_DEVICE_MODE_MASTER){
// if the device is master mode, then the SSI must also be 1 to avoid an error (unless Multi master?)
// this is only true if SSM is enabled, can we keep this functionality even is SSM is disabled?
// based on stm generated code, the SSI stays enabled for SSM enabled and disabled
tempReg |= pSPIHandle->SPIConfig.SPI_DeviceMode << SPI_CR1_MSTR;
tempReg |= (1 << SPI_CR1_SSI); // this should probably be done in another way
} else {
tempReg |= pSPIHandle->SPIConfig.SPI_DeviceMode << SPI_CR1_MSTR;
}
//2. configure the bus config
if(pSPIHandle->SPIConfig.SPI_BusConfig == SPI_BUS_CONFIG_FD){
//bdi should be cleared
tempReg &= ~(1 << SPI_CR1_BIDIMODE);
} else if(pSPIHandle->SPIConfig.SPI_BusConfig == SPI_BUS_CONFIG_HD){
//bdi should be set
tempReg |= ~(1 << SPI_CR1_BIDIMODE);
} else {
//bdi should be cleared
tempReg &= ~(1 << SPI_CR1_BIDIMODE);
//RXONLY should be set
tempReg |= (1 << SPI_CR1_RXONLY);
}
//3. configure the clock speed
tempReg |= (pSPIHandle->SPIConfig.SPI_SclkSpeed << SPI_CR1_BR);
//4. configure the Data frame format
tempReg |= (pSPIHandle->SPIConfig.SPI_DFF << SPI_CR1_DFF);
//5. configure the clock polarity
tempReg |= (pSPIHandle->SPIConfig.SPI_CPOL << SPI_CR1_CPOL);
//6. configure the clock phase
tempReg |= (pSPIHandle->SPIConfig.SPI_CPHA << SPI_CR1_CPHA);
//7. determine hardware or software slave management
tempReg |= (pSPIHandle->SPIConfig.SPI_SSM << SPI_CR1_SSM);
pSPIHandle->SPIx->CR1 = tempReg;
}
/******************************************************
* @fn - SPI_DeInit
*
* @brief - Deinitializes a given SPI periphal and resets register
*
* @param[in] - base address of SPI peripheral
*
* @return - none
*
* @Note - none
*/
void SPI_DeInit(SPI_RegDef_t* pSPIx){
if(pSPIx == SPI1){
SPI1_REG_RESET();
} else if (pSPIx == SPI2){
SPI2_REG_RESET();
} else if (pSPIx == SPI3){
SPI3_REG_RESET();
}
}
/*
* Data send and receive
*/
uint8_t SPI_GetFlagStatus(SPI_RegDef_t* pSPIx, uint32_t flag){
if(pSPIx->SR & flag){
return SET;
} else {
return RESET;
}
}
/******************************************************
* @fn - SPI_SendData
*
* @brief - sends data of length len to transmit buffer
*
* @param[in] - base address of SPI peripheral
* @param[in] - address of Tx buffer
* @param[in] - length of data to send
*
* @return - none
*
* @Note - This is a blocking call
*/
//actual code seems much more complex, check that out
void SPI_SendData(SPI_RegDef_t* pSPIx, uint8_t *pTxBuffer, uint32_t len){
//if SPi is not enabled, then enable it
if((pSPIx->CR1 & (1 << SPI_CR1_SPE)) != (1 << SPI_CR1_SPE)){
SET_BIT(pSPIx->CR1, (1 << SPI_CR1_SPE)); // this is repetitive, should just have a mask,
}
while(len > 0) {
//1. Wait until Tx is empty
while(SPI_GetFlagStatus(pSPIx, SPI_TXE_FLAG) == RESET);
//2. Check DFF in CR1
if (pSPIx->CR1 & (1 << SPI_CR1_DFF) ) {
//16 bit dff
//3. load data into DR
pSPIx->DR = *((uint16_t*)pTxBuffer);
len--;
len--;
(uint16_t*)pTxBuffer++;
} else {
//8 bit dff
//3. load data into DR
pSPIx->DR = *pTxBuffer;
len--;
pTxBuffer++;
}
}
}
/******************************************************
* @fn - SPI_ReceiveData
*
* @brief - Receives data of length len to Receive buffer
*
* @param[in] - base address of SPI peripheral
* @param[in] - address of Rx buffer
* @param[in] - length of data to Receive
*
* @return - none
*
* @Note - none
*/
void SPI_ReceiveData(SPI_RegDef_t* pSPIx, uint8_t *pRxBuffer, uint32_t len){
}
/*
* IRQ configuration and handling
*/
/******************************************************
* @fn - SPI_IRQITConfig
*
* @brief - enables or disables a SPI peripherals IRQ functionality
*
* @param[in] - IRQ number of SPI peripheral
* @param[in] - ENABLE or DISABLE macros
*
* @return - none
*
* @Note - none
*/
void SPI_IRQInterruptConfig(uint8_t IRQNumber, uint8_t enOrDi){
}
/******************************************************
* @fn - SPI_IRQPriorityConfig
*
* @brief - sets the priority for a given IRQ number
*
* @param[in] - IRQ priority of SPI peripheral
* @param[in] - IRQ number of SPI peripheral
*
* @return - none
*
* @Note - modifies the NVIC peripheral
*/
void SPI_IRQPriorityConfig(uint8_t IRQPriority, uint8_t IRQNumber){
}
/******************************************************
* @fn - SPI_IRQHandling
*
* @brief - deals with a interrupt triggered by a SPI pin
*
* @param[in] - Struct holding base address and desired configurations of SPI peripheral
*
* @return - none
*
* @Note - none
*/
void SPI_IRQHandling(SPI_Handle_t *pHandle){
}
/******************************************************
* @fn - SPI_PeripheralControl
*
* @brief - Enables or Disables SPI
*
* @param[in] - Pointer to a SPI register
*
* @return - none
*
* @Note - I'm implementing this in the transmit function
*/
void SPI_PeripheralControl(SPI_RegDef_t *pSPIx, uint8_t enOrDi){
if(enOrDi == ENABLE){
pSPIx->CR1 |= (1 << SPI_CR1_SPE);
} else {
pSPIx->CR1 &= ~(1 << SPI_CR1_SPE);
}
}
/******************************************************
* @fn - SPI_SSIConfig
*
* @brief - Sets or Resets SSI bit in SPI CR1 reg
*
* @param[in] - Pointer to a SPI register
*
* @return - none
*
* @Note - This code is used in the course I'm following,
* but I think it is redundant and bad practice. It will not be used.
* Instead, I implemented a check in the SPI init function to enable SSI w/ master
*/
void SPI_SSIConfig(SPI_RegDef_t *pSPIx, uint8_t enOrDi){
if(enOrDi == ENABLE){
pSPIx->CR1 |= (1 << SPI_CR1_SSI);
} else {
pSPIx->CR1 &= ~(1 << SPI_CR1_SSI);
}
}
/******************************************************
* @fn - SPI_SSOEConfig
*
* @brief - Sets or Resets SSOE bit in SPI CR2 reg
*
* @param[in] - Pointer to a SPI register
*
* @return - none
*
* @Note - This code is used in the course I'm following,
* but I think it is redundant and bad practice. I will use it bec of consistency
* Otherwise, in the STM generated code, this bit is determine by a NSS value in the init struct
* See: WRITE_REG(hspi->Instance->CR2, (((hspi->Init.NSS >> 16U) & SPI_CR2_SSOE) | hspi->Init.TIMode));
*/
void SPI_SSOEConfig(SPI_RegDef_t *pSPIx, uint8_t enOrDi){
if(enOrDi == ENABLE){
pSPIx->CR2 |= (1 << SPI_CR2_SSOE);
} else {
pSPIx->CR2 &= ~(1 << SPI_CR2_SSOE);
}
}
解决方案
推荐阅读
- python - 如何让 numpy.where() 只返回满足条件的元素?
- php - 未捕获的错误:找不到类“SQLite3”
- python - json.decoder.JSONDecodeError:预期值:第 1890 行第 29 列(字符 83535)
- android - 如何发送 recyclerview 项目的改造后请求?
- heroku - 有没有办法在 Heroku 上同时部署 SSR nodejs 应用程序和 Golang 应用程序在同一个测功机上?
- encoding - 从监督 LSTM 中提取向量编码
- php - 如何使 Wordpress 搜索不敏感
- html - 在 Elixir 中有条件地渲染组件的一部分
- ocaml - 如何阅读这段使用“~”和“:”的 OCaml 代码?
- postgresql - 根据 postgres 中的字段从两个表中获取唯一数据