赞
踩
RT-Thread (2) RTT SPI设备驱动流程 || LWIP + ENC28J60
RT-Thread (3) 为RTT增加SP485驱动||RTT UART设备
目录
在上一篇内容中,我适配了STM32的外部SRAM,并将其加载到内存管理中。我目前适配RTT的板子是用于工业物联网远程控制的,因此板载了一个以太网口。我们需要开启RTT的lwip软件包。
由于本文中包含一些RTT源码解析,如果只想按步骤操作的话请按以下顺序查看
1.1 配置ENC28J60接口
1.3 补充HAL_SPI_MspInit
2 LWIP配置
RT-Thread开发指南
RT-Thread Studio环境下lwIP+ENC28J60的启用与调试
MCU:STM32F103ZET6
外部RAM:XM8A51216
以太网接口芯片:ENC28J60 (10Mbps)
开发环境:RT-Thread Studio
RT-Thread:4.0.3
lwip:2.0.3
在上一篇中,由于RTT并没有FSMC类型的设备,所以我们并没有涉及到RTT的驱动框架。在这里我们补充一下RTT的启动流程。
RTT的入口在【rt-thread→src→components.c】中调用了rtthread_startup();:

转到定义rtthread_startup()的地方,同样在该文件中,如下:
- int rtthread_startup(void)
- {
- rt_hw_interrupt_disable();
-
- /* board level initialization
- * NOTE: please initialize heap inside board initialization.
- */
- rt_hw_board_init();
-
- /* show RT-Thread version */
- rt_show_version();
-
- /* timer system initialization */
- rt_system_timer_init();
-
- /* scheduler system initialization */
- rt_system_scheduler_init();
-
- #ifdef RT_USING_SIGNALS
- /* signal system initialization */
- rt_system_signal_init();
- #endif
-
- /* create init_thread */
- rt_application_init();
-
- /* timer thread initialization */
- rt_system_timer_thread_init();
-
- /* idle thread initialization */
- rt_thread_idle_init();
-
- #ifdef RT_USING_SMP
- rt_hw_spin_lock(&_cpus_lock);
- #endif /*RT_USING_SMP*/
-
- /* start scheduler */
- rt_system_scheduler_start();
-
- /* never reach here */
- return 0;
- }

RTT依次对各级进行初始化,初始化顺序参考RTT手册:

我们转到rt_hw_board_init()的定义。该函数在【driver→board.c】中。如下:
- RT_WEAK void rt_hw_board_init()
- {
- extern void hw_board_init(char *clock_src, int32_t clock_src_freq, int32_t clock_target_freq);
-
- /* Heap initialization */
- #if defined(RT_USING_HEAP)
- rt_system_heap_init((void *) HEAP_BEGIN, (void *) HEAP_END);
- #endif
-
- hw_board_init(BSP_CLOCK_SOURCE, BSP_CLOCK_SOURCE_FREQ_MHZ, BSP_CLOCK_SYSTEM_FREQ_MHZ);
-
- /* Set the shell console output device */
- #if defined(RT_USING_DEVICE) && defined(RT_USING_CONSOLE)
- rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
- #endif
-
- /* Board underlying hardware initialization */
- #ifdef RT_USING_COMPONENTS_INIT
- rt_components_board_init();
- #endif
-
- }

其中有我们上次讲到的内存堆初始化,hw_board_init(...)则是板级硬件的初始化。我们转到该函数的定义。在【driver→drv_common.c】中,如下:

其中终于涉及到了HAL库相关初始化的东西,我们继续转到HAL_Init()的定义。该定义在【项目\libraries\STM32F1xx_HAL_Driver\Src\stm32f1xx_hal.c】中。

我们可以看到在HAL_Init()中前面都是对系统时钟以及中断进行初始化,而在第163行调用了HAL_MspInit()。继续转到HAL_MspInit()的定义。在同文件下面有一个定义,然而这个定义是一个weak类型的,也就是说如果其他地方定义了一个不是weak类型的HAL_MspInit()则执行那个。那么这个文件中的HAL_MspInit()是没意义的,我们在工程中检索一下。发现项目中并没有其他定义,那么RTT是在什么时候初始化相关硬件的呢?
其实,我们只需要打开【项目\driver\drv_spi.c】,可以在里面找到rt_hw_spi_init(...),并且通过INIT_BOARD_EXPORT(rt_hw_spi_init)使其在板级初始化自动运行。也就是在【项目\drvier\board.c】中的rt_hw_board_init()调用的rt_components_board_init()中执行。

如下图所示,为ENC28J60的电路图以及与STM32的接口。

ENC28J60与STM32之间采用SPI接口。在这里我们不直接使用STM32的SPI驱动程序对接ENC28J60的接口,而是使用RTT的接口驱动。
① 打开项目的RT-Thread Settings。
② 使能SPI总线,并使能RTT自带的ENC28J60 SPI以太网接口

我们使能ENC28J60的接口之后,RTT的lwip协议栈也自动被使能。这是因为ENC28J60接口依赖lwip协议栈。

保存配置后,在项目工程的【rt-thread】→【components】→【net】目录下可以看到【lwip-2.0.2】的相关文件。

lwip的部分我们先不管,首先解决ENC28J60的驱动问题。
③ 在board.h中启用SPI1
ENC28J60在我的板子上是挂载到SPI1接口的,所以我们首先打开board.h。搜索#define BSP_USING_SPI1并取消该注释。取消该注释之后即启用了RTT的spi1驱动。
④ 在stm32f1xx_hal_conf.h中取消#define HAL_SPI_MODULE_ENABLED的注释
这个是启用SPI的HAL库。
⑤ 在【driver】目录下新建drv_enc28j60.c文件
在该文件中,我们需要调用RTT的SPI设备驱动,挂载ENC28J60。
如下图所示,ENC28J60有6个I/O口,其中REST复位引脚可不管,因为RTT的ENC28J60驱动使用软复位。

而后在drv_enc28j60.c中添加如下程序:
- #include
- #include
- #include
-
- #define ENC28J60_INT 102//PG6 ENC28J60中断
- static int rt_hw_enc28j60_init(void)
- {
- rt_hw_spi_device_attach("spi1", "enc28", GPIOG, GPIO_PIN_7);//PG7 ENC28J60片选CS
- enc28j60_attach("enc28");
- rt_pin_mode(ENC28J60_INT, PIN_MODE_INPUT_PULLUP);
- rt_pin_attach_irq(ENC28J60_INT, PIN_IRQ_MODE_FALLING, (void(*)(void*))enc28j60_isr, RT_NULL);
- rt_pin_irq_enable(ENC28J60_INT, PIN_IRQ_ENABLE);
- return 0;
- }
- INIT_COMPONENT_EXPORT(rt_hw_enc28j60_init);
首先,我们通过rt_hw_spi_device_attach("spi1", "spi10", GPIOC, GPIO_PIN_4)配置SPI1总线上的enc28设备使用PG7作为片选引脚CS。
而后使用enc28j60_attach("enc28")挂载enc28j60,该函数由【项目\rt-thread\components\drivers\spi\enc28j60.c】提供,暂时就不展开了。
而后设置enc28j60的中断引脚。在这里通过宏定义 #define ENC28J60_INT 102定义。102是RTT中PG6引脚的编号,可以在【项目\drvier\drv_gpio.c】中查找到。
通过rt_pin_mode(ENC28J60_INT, PIN_MODE_INPUT_PULLUP)设置102号引脚为上拉输入。
通过rt_pin_attach_irq(ENC28J60_INT, PIN_IRQ_MODE_FALLING, (void()(void))enc28j60_isr, RT_NULL)设置102号引脚为下降沿触发模式,并设置中断服务函数为enc28j60_isr。
最后通过rt_pin_irq_enable(ENC28J60_INT, PIN_IRQ_ENABLE)使能外部中断。
通过INIT_COMPONENT_EXPORT(rt_hw_enc28j60_init)导入到组件级自动初始化。
我们经过第0.3小节的查找得知,RTT是在板级初始化时自动运行spi初始化程序。那么接下来我们需要了解一下RTT究竟初始化了什么,有没有需要我们补充的。
我们转到【项目\driver\drv_spi.c】,可以在里面找到rt_hw_spi_init(...),其内容如下:
- int rt_hw_spi_init(void)
- {
- stm32_get_dma_info();
- return rt_hw_spi_bus_init();
- }
- INIT_BOARD_EXPORT(rt_hw_spi_init);
可以看到里面第一个是配置了SPIDMA相关,第二个则是初始spi总线。我们转到rt_hw_spi_bus_init()。整个函数比较长,这里只放一部分。如下:
- static int rt_hw_spi_bus_init(void)
- {
- rt_err_t result;
- for (int i = 0; i < sizeof(spi_config) / sizeof(spi_config[0]); i++)
- {
- spi_bus_obj[i].config = &spi_config[i];
- spi_bus_obj[i].spi_bus.parent.user_data = &spi_config[i];
- spi_bus_obj[i].handle.Instance = spi_config[i].Instance;
可以明显的看出这是一个遍历的结构,spi_config[]数组中保存的就是我们的SPI总线列表。也在同文件中定义。可以看到我们只在board.h中启用了SPI1.所以目前列表中只有SPI1.

在rt_hw_spi_bus_init()的最后,调用了rt_spi_bus_register(...),并传入了一个结构体stm_spi_ops。该结构体内容如下:
- static const struct rt_spi_ops stm_spi_ops =
- {
- .configure = spi_configure,
- .xfer = spixfer,
- };
至于内核如何调用结构体我们就不管了,但是我们知道 spi_configure这个函数被传递过去了。我们去看一下spi_configure的内容。
- static rt_err_t spi_configure(struct rt_spi_device *device,
- struct rt_spi_configuration *configuration)
- {
- RT_ASSERT(device != RT_NULL);
- RT_ASSERT(configuration != RT_NULL);
-
- struct stm32_spi *spi_drv = rt_container_of(device->bus, struct stm32_spi, spi_bus);
- spi_drv->cfg = configuration;
-
- return stm32_spi_init(spi_drv, configuration);
- }
可以看到,里面调用了stm32_spi_init,stm32_spi_init中调用了HAL_SPI_Init(spi_handle)。
HAL_SPI_Init定义在stm32f1xx_hal_spi.c中,里面没有对SPI的IO口进行初始化,但是里面又调用了HAL_SPI_MspInit,可是HAL_SPI_MspInit是一个__weak函数,里面没有内容。
这意味着RTT没有初始化SPI1的端口,需要我们补充!!!!
我们打开board.c,在里面添加如下内容:
- void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
- {
- GPIO_InitTypeDef GPIO_InitStruct = {0};
- if(hspi->Instance==SPI1)
- {
- /* 外设时钟使能 */
- __HAL_RCC_SPI1_CLK_ENABLE();
-
- __HAL_RCC_GPIOA_CLK_ENABLE();
- /**SPI1 GPIO 配置
- PA5 ------> SPI1_SCK
- PA6 ------> SPI1_MISO
- PA7 ------> SPI1_MOSI
- */
- GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7;
- GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
- HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- GPIO_InitStruct.Pin = GPIO_PIN_6;
- GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
- GPIO_InitStruct.Pull = GPIO_NOPULL;
- HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
- }
- }

这里我们只补充了SPI1的相关内容,以后如果需要其他SPI接口,则继续补充。
lwip版本我们选择2.0.2,以便获得良好的兼容性。

①首先,我们使用了ENC28J60作为网络接口设备,所以我们要打开RTT配置中【网络】【网络接口设备】【使能网络接口设备】。

②修改内存占用相关参数
PBUF的数量
系统默认为16个,修改为8个,一个PBUF要占用1576个字节,而且是静态分配的,PBUF数量越多,接收速度就越快,但也越耗内存。
TCP发送窗口的大小、发送缓冲区的大小
这两项都是动态分配的,修改后能减少一半的内存占用。

③ 是否使能DCHP自动获取IP地址
自动获取IP

静态IP

④ 是否启用ping功能

完成后编译工程,编译后工程大小为:

我使用的是DHCP自动获取IP,然后通过中断输入ifconfig命令查看网络状态,如下图所示,可以看到已经自动获取到了IP地址。

使用ping命令ping www.baidu.com测试网络连接,结果如下所示:

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。