STM32裸机编程指南-12

系列目录

带设备仪表盘的网络服务器

Nucleo-F429ZI 带有板载以太网。以太网硬件需要两个组件:PHY(向铜缆、光缆等介质发送和接收电信号)和 MAC(驱动 PHY 控制器)。
在我们的Nucleo开发板上,MAC控制器是MCU内置的,PHY是外部的(具体来说,是Microchip的LAN8720a)。

MAC和PHY可以用多个接口通信,我们将使用RMII。为此,一些引脚必须配置为使用其替代功能 (AF)。要实现 Web 服务器,我们需要 3 个软件组件:

  • 网络驱动程序,用于向 MAC 控制器发送/接收以太网帧
  • 一个网络堆栈,用于解析帧并理解 TCP/IP
  • 理解HTTP的网络库

我们将使用猫鼬网络库,它在单个文件中实现所有这些。这是一个双重许可的库(GPLv2/商业),旨在使网络嵌入式开发快速简便。

先拷贝 mongoose.cmongoose.h 到我们的工程中,现在我们手上有网络驱动、网络协议栈和HTTP库了,Mongoose还提供了很多示例,其中之一是设备仪表盘示例。这个示例实现了很多事情,像登录、通过WebSocket实时传输数据、嵌入式文件系统、MQTT通信等等,我们就使用这个例子,再拷贝2个文件:

我们需要告诉 Mongoose 开启哪些功能,可以通过设置预处理常数等编译器标记实现,也可以在 mongoose_custom.h 文件中设置。我们用第二种方法,创建 mongoose_custom.h 文件并写入以下内容:

1
2
3
4
5
6
#pragma once
#define MG_ARCH MG_ARCH_NEWLIB
#define MG_ENABLE_MIP 1
#define MG_ENABLE_PACKED_FS 1
#define MG_IO_SIZE 512
#define MG_ENABLE_CUSTOM_MILLIS 1

现在向 main.c 添加一些网络代码,#include "mongoose.c" 初始化以太网RMII引脚,并在RCC中使能以太网:

1
2
3
4
5
6
7
8
9
10
11
12
13
uint16_t pins[] = {PIN('A', 1),  PIN('A', 2),  PIN('A', 7),
PIN('B', 13), PIN('C', 1), PIN('C', 4),
PIN('C', 5), PIN('G', 11), PIN('G', 13)};
for (size_t i = 0; i < sizeof(pins) / sizeof(pins[0]); i++) {
gpio_init(pins[i], GPIO_MODE_AF, GPIO_OTYPE_PUSH_PULL, GPIO_SPEED_INSANE,
GPIO_PULL_NONE, 11);
}
nvic_enable_irq(61); // Setup Ethernet IRQ handler
RCC->APB2ENR |= BIT(14); // Enable SYSCFG
SYSCFG->PMC |= BIT(23); // Use RMII. Goes first!
RCC->AHB1ENR |= BIT(25) | BIT(26) | BIT(27); // Enable Ethernet clocks
RCC->AHB1RSTR |= BIT(25); // ETHMAC force reset
RCC->AHB1RSTR &= ~BIT(25); // ETHMAC release reset

Mongoose的驱动程序使用以太网中断,因此我们需要更新 startup.c 并将 ETH_IRQHandler 添加到向量表中。让我们以不需要任何修改就能添加中断处理函数的方式重新组织 startup.c 中的向量表定义,方法是使用“弱符号”概念。

函数可以标记为“弱”,它的工作方式与普通函数类似。当源代码定义具有相同名称的函数时,差异就来了。通常,两个同名的函数会构建失败。但是,如果一个函数被标记为弱函数,则可以构建成功并且链接器会选择非弱函数。这提供了设置样板中的函数为“默认函数”的能力,然后可以在代码中的其他位置简单地创建一个同名函数来覆盖它。

我们接下来用这种方法填充向量表,创建一个 DefaultIRQHandler() 并标记为weak,然后给每一个中断处理函数声明一个处理函数名并使它成为 DefaultIRQHandler() 的别名:

1
2
3
4
5
6
7
8
9
10
11
12
void __attribute__((weak)) DefaultIRQHandler(void) {
for (;;) (void) 0;
}
#define WEAK_ALIAS __attribute__((weak, alias("DefaultIRQHandler")))

WEAK_ALIAS void NMI_Handler(void);
WEAK_ALIAS void HardFault_Handler(void);
WEAK_ALIAS void MemManage_Handler(void);
...
__attribute__((section(".vectors"))) void (*tab[16 + 91])(void) = {
0, _reset, NMI_Handler, HardFault_Handler, MemManage_Handler,
...

现在,我们可以在代码中定义任何中断处理函数,它会替代默认的那个。这就是我们的例子中所发生的:Mongoose的STM32驱动中定义了一个 ETH_IRQHandler(),它会替代默认的中断处理函数。

下一步是初始化Mongoose库:创建时间管理器、配置网络驱动、启动监听HTTP连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct mg_mgr mgr;        // Initialise Mongoose event manager
mg_mgr_init(&mgr); // and attach it to the MIP interface
mg_log_set(MG_LL_DEBUG); // Set log level

struct mip_driver_stm32 driver_data = {.mdc_cr = 4}; // See driver_stm32.h
struct mip_if mif = {
.mac = {2, 0, 1, 2, 3, 5},
.use_dhcp = true,
.driver = &mip_driver_stm32,
.driver_data = &driver_data,
};
mip_init(&mgr, &mif);
extern void device_dashboard_fn(struct mg_connection *, int, void *, void *);
mg_http_listen(&mgr, "http://0.0.0.0", device_dashboard_fn, &mgr);
MG_INFO(("Init done, starting main loop"));

剩下的就是把 mg_mgr_poll() 调用加到主循环。

现在把 mongoose.cnet.cpacked_fs.c 文件加到Makefile,重新构建,烧写到板子上。连接一个串口控制台到调试输出,可以观察到板子通过DHCP获取了IP地址:

1
2
3
4
5
6
847 3 mongoose.c:6784:arp_cache_add     ARP cache: added 0xc0a80001 @ 90:5c:44:55:19:8b
84e 2 mongoose.c:6817:onstatechange READY, IP: 192.168.0.24
854 2 mongoose.c:6818:onstatechange GW: 192.168.0.1
859 2 mongoose.c:6819:onstatechange Lease: 86363 sec
LED: 1, tick: 2262
LED: 0, tick: 2512

打开一个浏览器,输入上面的IP地址,就可以看到一个仪表盘。在full description获取更多细节。

Device dashboard

完整工程源码可以 step-7-webserver 文件夹找到。