MCU全套代码工程

最近在整理手头的MCU项目代码,突然发现很多新人拿到开发板后只会用现成的HAL库点灯。今天咱们就聊聊怎么从裸机开始搭个完整的MCU工程,绝对比直接复制例程有意思多了。

先看工程目录结构:

Project/
├── Startup/       # 启动文件
├── Driver/        # 外设驱动
├── Middlewares/   # 中间件
├── Application/   # 应用层
└── Makefile       # 老派的编译方式

启动文件这个黑盒子得拆开看看。以STM32F103的startup_stm32f103xe.s为例,关键部分长这样:

Reset_Handler:
    ldr   sp, =_estack       ; 设置堆栈指针
    bl    SystemInit         ; 系统时钟初始化
    bl    __libc_init_array  ; C库初始化
    bl    main               ; 跳转到C的世界
    bx    lr

这里有个坑,_estack定义在链接脚本里,要是堆栈设太小,跑RTOS分分钟死给你看。之前有个项目因为ADC采样开大数组,直接爆栈导致HardFault,调试了两天才发现是这里没改。

外设驱动咱们自己封装个GPIO操作试试:

// Driver/gpio.c
void GPIO_Toggle(GPIO_TypeDef* GPIOx, uint16_t Pin)
{
    static uint8_t pin_state[16] = {0};
    
    if(pin_state[Pin] == 0){
        GPIOx->BSRR = (1 << Pin);  // 置位
        pin_state[Pin] = 1;
    } else {
        GPIOx->BSRR = (1 << (Pin + 16));  // 复位
        pin_state[Pin] = 0;
    }
}

这个实现比HAL库的HALGPIOTogglePin()骚在哪?注意静态变量保存状态,直接操作寄存器BSRR原子性地切换电平。用HAL库的话得先读当前状态再写,中间可能被中断打断导致逻辑错乱。不过要注意GPIOx的时钟必须提前使能,否则直接操作寄存器会卡死。

MCU全套代码工程

中间件层举个FreeRTOS的任务创建例子:

// Middlewares/rtos_tasks.c
void StartDefaultTask(void const * argument)
{
    /* 这里放初始化代码 */
    for(;;)
    {
        GPIO_Toggle(LED_GPIO_Port, LED_Pin);
        vTaskDelay(500);  // 不用HAL_Delay是怕阻塞其他任务
    }
}

xTaskCreate(StartDefaultTask, "LED_Task", 128, NULL, 1, NULL);

注意任务栈大小设128字(不是字节!),之前有哥们把栈大小当字节设置,结果跑着跑着任务自己消失了。用vTaskDelay而不是HAL_Delay是为了让出CPU时间,毕竟RTOS里阻塞式延时是大忌。

最后在应用层main函数里把这些串起来:

// Application/main.c
int main(void)
{
    SystemClock_Config();  // 自己实现的时钟配置
    MX_GPIO_Init();        // 自动生成的初始化
    osKernelStart();       // 启动RTOS内核
    
    while(1)
    {
        /* 这里永远不会执行到 */
    }
}

这个while(1)看起来多余?其实有些RTOS在空闲时会回到这里。曾经遇到个奇葩问题,某款国产MCU的osKernelStart()实现有bug,不加这个死循环直接跑飞。

整个工程编译建议用Makefile而不是IDE,这样换芯片型号时改起来方便。不过要处理那些该死的依赖关系,可以试试这个骚操作:

C_SOURCES = $(shell find ./ -name '*.c')
# 生成依赖文件
DEPFILES = $(C_SOURCES:%.c=%.d)
-include $(DEPFILES)

这样修改头文件后会自动触发重新编译相关源文件,比IDE的自动检测还灵敏。不过第一次生成依赖文件时可能会报错,加个--keep-going参数就能跳过了。

最后说个血泪教训:千万别在中断服务函数里调用自己写的GPIO_Toggle()!之前有个项目在USART中断里调了这个函数,结果因为优先级问题导致中断嵌套,直接把系统干趴了。后来用逻辑分析仪抓波形才发现,中断里操作GPIO的时间比想象中长得多。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐