本帖最后由 xhackerustc 于 2025-3-8 20:43 编辑
#申请原创#
@21小跑堂
RA2E1基于Arm Cortex-M23,频率最高48MHZ,FLASH最大能到128kB,SRAM最大有16kB。这款产品采用优化的制程和瑞萨电子的低功耗工艺技术,超低功耗表现很牛。RA2E1支持1.6V到5.5V宽电压,很多情况下可以省去电平转换芯片,节省BOM,很赞的一个特性。总的来说RA2E1比较适合电池供电需要低功耗的
系统,比如耳温枪、遥控器、电子秤、血糖仪、智能台灯等等。笔者这块RA-Eco-RA2E1开发板由RA生态工作室出品(话说他家出了好多好多基于RA芯片的开发板,笔者看得口水直流,看看这篇文章能不能把他家吸引来二姨家开个版面放开测评机会 ),基于RA2E1家族的R7FA2E1A7,64KB Flash,16KB SRAM,引出了全部引脚,适合开发阶段使用。开发板未板载调试器(其实笔者一直觉得板载调试器是比较浪费的,不如板载spi nor flash、can收发器等更为合算,又可充分利用资源),虽然瑞萨有串口烧录工具,但为方便开发调试,最好准备一个daplink兼容的调试器,市面上很多选择。
话说这是笔者第一次使用基于Cortex-M23的开发板,板子到手后把玩了好多天,对RA2E1的SPI比较感兴趣,以前也没用过RA系的SPI,现在想体验一把并做个性能测试。
RA2E1 SPI介绍
RA2E1其实有两个SPI控制器:由SCI可以转成简易SPI控制器,另一个是原生的SPI控制器。本文重点是后者。此SPI控制器一些关键特性如下:
支持Master和Slave模式;
4Byte的收/发FIFO;
SCLK极性和相位均可调;
支持MSB或LSB,传输比特长度可支持8、9、10、11、12、13、14、15、16、20、24、32bit,注这里不>是传输字节长度,而是bit长度,8bit最常见,但有些SPI设备bit长不是8bit的,可能是9bit、10bit等。一次SPI传输的数据是不限的,可改变,是传输bit长的倍数;
最重要的一个等式是关于传输比特率的:
Bit Rate = f(PCLKB)/(2(n+1)2^N)
其中n是SPBR的“n”,N是BRDV的“N”,详见官方数据手册
RA系spi api简介
此函数用来打开并初始化SPI控制器,参数由RASC配置好,RASC使用见下文。
此函数用来发起SPI传输向slave设备写入。第二个参数指定传输buf,第四个参数是上文提到的传输bit长,第三个参数意思是传输多少个bit长。
此函数用来发起SPI传输从slave读取。参数意义和R_SPI_Write()一致
此函数用来关闭SPI控制器并disable相应中断
由此可见关于SPI API的使用还是比较简单的,厂商都封装好了。
测试用psram介绍
笔者这款psram模块基于ESP-PSRAM64H,3.3V电压,8MB容量,最高频率可达133MHZ,支持标准SPI模式和Quad SPI模式。
RASC中SPI相关配置
RASC全称Renesas Advanced Smart Configurator,是瑞萨用于配置他自家芯片的pll/clk,pinmux、中断等的工具,会用CubeMX就会用RASC。在RA2E1 SPI配置时,主要注意三点:
使能SPI pin
使能一个gpio
此gpio控制用于psram的片选信号,据psram手册,片选需要程序控制。笔者选p500,这根引脚在板子排针上和p100等靠得很近,方便接杜邦线。
stacks栏配置spi
特别注意回调函数名称,笔者用的名字是spi_callback
SEGGER RTT的使用
因为spi接psram模块本来就需要MOSI、MISO、CLK三根线,再考虑到vcc、gnd、片选,总共要6根线,如果用串口打印的话需要再接3根线,接线能做但挺繁的,所以这时候SEGGER_RTT就派上用场了,只要接上SWD相应引脚即可烧录调试又能获得RTT打印支持。SEGGER RTT使用过程非本文重点,步骤在此略过,其实挺简单的。
计时的考虑
因为要测试性能,所以需要计时。但CM23无DWT,不得已只能上systick了,因systick只有24位,48MHZ时钟下0.3s多一点也就需要reload了,所以这里计时需要考虑在SysTick_Handler()里做个计数,综合算时间。笔者做法如下:
在初始化过程中以0.3s的中断间隔启动systick:
SysTick_Config(14400000); // 0.3s per interrupt @48MHZ
在SysTick_Handler()中,ticks加一计数:
static unsigned int ticks;
void SysTick_Handler(void)
{
++ticks;
}
获得us级时间的函数:
static uint32_t get_us()
{
return ticks * 300000 + (0xffffffu - SysTick->VAL) / 48;
}
SPI回调函数实现
static volatile bool g_transfer_complete;
void spi_callback(spi_callback_args_t *p_args)
{
if (SPI_EVENT_TRANSFER_COMPLETE == p_args->event)
g_transfer_complete = true;
}
瑞萨家的固件库中像串口、SPI、I2C等器件的API基本都是非阻塞的,是基于中断事件的,所以要想知道传输是否完成需要等待中断事件,OS环境中可由os原语实现,裸机环境就得poll一个标志位了,然后由回调函数调用os原语(对应OS环境)或设置标志位(对应裸机环境)即可。`
实现psram初始化
根据ESP-PSRAM64H的datasheet,它上电后自动初始化进入standby模式,不过笔者初始化后还是向PSRAM发送了RESET_EN和RESET命令,发命令的函数如下所示:
#define CMD_WRITE 0x02
#define CMD_READ 0x03
#define CMD_FAST_READ 0x0b
#define CMD_RESET_EN 0x66
#define CMD_RESET 0x99
#define CMD_READ_ID 0x9f
static int psram_send_cmd(const uint8_t cmd)
{
fsp_err_t err;
g_transfer_complete = false;
err = R_SPI_Write(&g_spi0_ctrl, &cmd, 1, SPI_BIT_WIDTH_8_BITS);
if (FSP_SUCCESS != err) {
SEGGER_RTT_printf(0, "psram_read_id write cmd failed\n");
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_00, BSP_IO_LEVEL_HIGH);
return -1;
}
/* Wait for SPI_EVENT_TRANSFER_COMPLETE callback event. */
while (g_transfer_complete == false) {;}
return 0;
}
读取psram id
static int psram_read_id(uint8_t *buf)
{
uint8_t cmd[4];
fsp_err_t err;
cmd[0] = CMD_READ_ID;
/* cmd[1], cmd[2] and cmd[3] are dummy address */
g_transfer_complete = false;
err = R_SPI_Write(&g_spi0_ctrl, cmd, 4, SPI_BIT_WIDTH_8_BITS);
if (FSP_SUCCESS != err) {
SEGGER_RTT_printf(0, "psram_read_id write cmd failed\n");
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_00, BSP_IO_LEVEL_HIGH);
return -1;
}
/* Wait for SPI_EVENT_TRANSFER_COMPLETE callback event. */
while (g_transfer_complete == false) {;}
g_transfer_complete = false;
err = R_SPI_Read(&g_spi0_ctrl, buf, 6, SPI_BIT_WIDTH_8_BITS);
if (FSP_SUCCESS != err) {
SEGGER_RTT_printf(0, "psram_read_id read ID failed\n");
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_00, BSP_IO_LEVEL_HIGH);
return -1;
}
/* Wait for SPI_EVENT_TRANSFER_COMPLETE callback event. */
while (g_transfer_complete == false) {;}
return 0;
}
实现psram的读取
int psram_read(uint32_t addr, void *buf, int len)
{
uint8_t cmd[5];
fsp_err_t err;
cmd[0] = CMD_FAST_READ;
cmd[1] = (uint8_t)(addr >> 16);
cmd[2] = (uint8_t)(addr >> 8);
cmd[3] = (uint8_t)(addr);
/* cmd[4] is dummy */
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_00, BSP_IO_LEVEL_LOW);
g_transfer_complete = false;
err = R_SPI_Write(&g_spi0_ctrl, cmd, 5, SPI_BIT_WIDTH_8_BITS);
if (FSP_SUCCESS != err) {
SEGGER_RTT_printf(0, "psram_read write cmd failed\n");
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_00, BSP_IO_LEVEL_HIGH);
return -1;
}
/* Wait for SPI_EVENT_TRANSFER_COMPLETE callback event. */
while (g_transfer_complete == false) {;}
g_transfer_complete = false;
err = R_SPI_Read(&g_spi0_ctrl, buf, len, SPI_BIT_WIDTH_8_BITS);
if (FSP_SUCCESS != err) {
SEGGER_RTT_printf(0, "psram_read read data failed %lx %d\n", addr, len);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_00, BSP_IO_LEVEL_HIGH);
return -1;
}
/* Wait for SPI_EVENT_TRANSFER_COMPLETE callback event. */
while (g_transfer_complete == false) {;}
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_00, BSP_IO_LEVEL_HIGH);
return len;
}
实现psram的写入
int psram_write(uint32_t addr, void *buf, int len)
{
uint8_t cmd[4];
fsp_err_t err;
cmd[0] = CMD_WRITE;
cmd[1] = (uint8_t)(addr >> 16);
cmd[2] = (uint8_t)(addr >> 8);
cmd[3] = (uint8_t)(addr);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_00, BSP_IO_LEVEL_LOW);
g_transfer_complete = false;
err = R_SPI_Write(&g_spi0_ctrl, cmd, 4, SPI_BIT_WIDTH_8_BITS);
if (FSP_SUCCESS != err) {
SEGGER_RTT_printf(0, "psram_write write cmd failed\n");
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_00, BSP_IO_LEVEL_HIGH);
return -1;
}
/* Wait for SPI_EVENT_TRANSFER_COMPLETE callback event. */
while (g_transfer_complete == false) {;}
g_transfer_complete = false;
err = R_SPI_Write(&g_spi0_ctrl, buf, len, SPI_BIT_WIDTH_8_BITS);
if (FSP_SUCCESS != err) {
SEGGER_RTT_printf(0, "psram_write write data failed %lx %d\n", addr, len);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_00, BSP_IO_LEVEL_HIGH);
return -1;
}
/* Wait for SPI_EVENT_TRANSFER_COMPLETE callback event. */
while (g_transfer_complete == false) {;}
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_00, BSP_IO_LEVEL_HIGH);
return len;
}
测试代码
void psram_test(void)
{
unsigned int i;
uint32_t addr;
uint32_t t1;
uint8_t testbuf[64];
psram_init();
memset(testbuf, 0x5a, sizeof(testbuf));
t1 = get_us();
for (i = 0; i < 10000; ++i) {
psram_write(i * 64, &testbuf, 64);
}
t1 = get_us() - t1;
t1 /= 1000;
SEGGER_RTT_printf(0, "PSRAM write speed: %lld B/s.\n", 64 * 10000 * 1000 / t1);
t1 = get_us();
for (i = 0; i < 10000; ++i) {
psram_read(i * 64, &testbuf, 64);
}
t1 = get_us() - t1;
t1 /= 1000;
SEGGER_RTT_printf(0, "PSRAM read speed: %lld B/s.\n", 64 * 10000 * 1000 / t1);
SEGGER_RTT_printf(0, "PSRAM test done\n");
}
编译
cmake -B /tmp/build
cmake --build /tmp/build -j8
在/tmp/build目录下有ra2e1.elf和ra2e1.srec文件生成
烧录
pyocd load -e sector -t R7FA2E1A7 /tmp/build/ra2e1.elf
pyocd获取rtt打印信息
pyocd rtt -t r7fa2e1a7 -a 0x20004a04
其中0x20004a04是_SEGGER_RTT符号地址,在编译结果的ra2e1.map中搜索即可获得
初步数据解析
烧录后板子重启,运行截图如下:
由此可见,写入速度是549828 Byte/s,读取速度是545144 Byte/s。因PCLKB是24MHZ,那么理论上最大Bit Rate按上文公式可达12Mbps,所以SPI总线理论速度传输速度大概能到1.5MB/s,实际速度大概只有理论速度的三分之一。下文做一些实验尝试提升SPI传输速度。
尝试提升PCLKB的频率
按官方手册,PCLKB的频率最高可到32MHZ,咱们在RASC配置试试。为了让PCLKB达32MHZ,可让HOCO频率升到64MHZ,然后2分频即可。但此时cpu最高只能48MHZ,所以只能两分频给ICLK,故cpu频率此时到32MHZ。编译烧录测试截图如下:
由此可见,读写速度反而下降了,猜测SPI传输瓶颈不在SPI总线上而在CPU上(毕竟现在的测试代码中是由CPU搬运数据),可能是CPU(只有32MHZ)来不及搬运导致。
尝试关闭param_checking
这个选项在RASC中可配置,测试结果基本不变。
尝试开启rxi_transmit
这个选项同样在RASC中可配置,测试截图如下:
由此可见在未使用dma引擎时rxi_transmit可提升SPI传输速度大概1个百分点。
思考与总结
其实如果由DMA引擎来搬运SPI数据,效率最高、速度最快。笔者搜索官方文档发现RA2E1集成了一个名叫DTC(data transfer controller)模块,但目前不太清楚这个DTC是否可以用来搬运SPI数据。所以如果DTC可用于搬运SPI数据,建议官方增加这样使用样例代码。
|