嵌入式系统入门

嵌入式系统入门及一些逻辑解析 开始-写下嵌入式学习历程的所思所得 阅读详情

怎么让DMA完成内存到多个外设的散波传输 定时器产生带死区的互补PWM波 系统节拍器更新所有任务的时间片 用示波器测电源完整性 PID的串级控制 寄存器位带操作 、精度芯片勘误表和编程手册 、手写RTOS内核、掌握Simulink和LabVIEW、Visio 状态机、中断、DMA实现零CPU占用传输

总体架构:从物理世界到代码世界

嵌入式系统的本质,是用数字逻辑去控制和感知模拟的物理世界。理解这一点,是理解下面所有技术的第一性原理。

从物理层看,电流在导体中流动、电压在节点上建立、电磁场在空间中传播。从数学层看,控制系统用微分方程描述动态行为,用离散化方法将连续世界映射到数字计算。从信息层看,数据在总线、存储器和外设之间流动,需要高效、可靠的搬运机制。

下面这张图展示了各模块之间的逻辑关系——它们不是孤立的,而是一个完整的嵌入式控制系统的有机组成部分:

(AI生成)

text

┌─────────────────────────────────────────────────────────────────┐
│                    嵌入式系统全景架构                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐  │
│  │  传感器   │───▶│  信号调理  │───▶│   ADC    │───▶│  DMA    │  │
│  │(物理量→电)│    │(滤波/放大)│    │(模拟→数字)│    │(数据搬运)│  │
│  └──────────┘    └──────────┘    └──────────┘    └────┬─────┘  │
│                                                        │         │
│                                                        ▼         │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    CPU / MCU 核心                          │   │
│  │  ┌─────────┐  ┌──────────┐  ┌──────────┐  ┌───────────┐  │   │
│  │  │ RTOS    │  │ PID控制  │  │ 状态机   │  │ 位带操作  │  │   │
│  │  │ 调度器  │  │ 算法     │  │ 逻辑     │  │ 寄存器    │  │   │
│  │  └────┬────┘  └────┬─────┘  └──────────┘  └───────────┘  │   │
│  └───────┼────────────┼──────────────────────────────────────┘   │
│          │            │                                           │
│          ▼            ▼                                           │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐   │
│  │  定时器   │───▶│  PWM     │───▶│  驱动电路 │───▶│  执行器   │   │
│  │(计时/死区)│    │(互补输出)│    │(功率放大)│    │(电机/加热)│   │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘   │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              开发保障层                                    │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐              │   │
│  │  │ 芯片勘误  │  │ 示波器   │  │ 代码工具  │              │   │
│  │  │ + 编程手册│  │ 电源完整性│  │ Simulink  │              │   │
│  │  └──────────┘  └──────────┘  └──────────┘              │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

贯穿全文的第一性原理:嵌入式系统的每一个设计决策,本质上都是在时间、空间和精度之间做权衡。DMA用硬件换CPU时间;死区插入用微小的时间损失换取功率器件安全;RTOS用上下文切换开销换取任务响应性;位带操作用地址空间换原子性。理解了这个“三角权衡”,你就掌握了嵌入式系统设计的灵魂。

一、DMA:让数据搬运不再消耗CPU

1.1 第一性原理:为什么需要DMA?

数学视角:假设你有一个ADC以1MHz的速率采样,每次采样产生2字节数据。如果CPU用轮询方式搬运数据,假设每次搬运需要10个时钟周期,CPU在100MHz时钟下,每秒需要消耗 1M × 10 = 1000万个时钟周期,也就是10%的CPU算力被纯粹的数据搬运吃掉了。更糟糕的是,轮询是同步的——CPU必须时刻盯着ADC,无法同时做别的事。

物理类比:DMA就像一个专业的搬运工。CPU是“大脑”,擅长思考和决策;DMA是“手脚”,擅长重复性劳动。大脑让搬运工把货物从仓库A搬到仓库B,然后就可以去思考别的策略了。搬运工搬完后报告一声即可。

核心公式:总线带宽利用率 = 有效数据传输量 / (有效数据传输量 + 总线仲裁和开销)

DMA的目标就是让这个比值趋近于1。

1.2 关键技术点

传输方向与地址模式:DMA支持外设到存储器、存储器到外设、存储器到存储器三种方向。对于“内存到多个外设”的散波传输,核心思想是:同一块内存数据,依次搬运到多个外设的数据寄存器。

这需要理解DMA的两个地址指针:

  • 源地址指针:指向内存中的同一位置(不递增,因为每次取同一份数据)

  • 目标地址指针:初始指向第一个外设的数据寄存器,搬运N次后切换到第二个外设

STM32的DMA控制器通过DMA_SxCR寄存器中的PINC(外设地址递增)和MINC(存储器地址递增)位来控制地址是否自动递增。

循环模式与双缓冲:CIRC位使能循环模式后,DMA在一次传输完成后自动重新开始,周而复始。DBM位使能双缓冲模式后,DMA在两个存储器缓冲区之间自动切换,CPU可以在DMA读写一个缓冲区时处理另一个缓冲区的数据,实现真正的零等待。

1.3 “内存到多个外设”散波传输的配置思路

这里给出一个清晰的心智模型:把DMA想象成一个分发员,手里拿着一叠相同的传单(内存数据),要分发给多个不同的窗口(外设)。

分步配置:

  1. 选择DMA控制器和通道:不同外设映射到不同的DMA通道,需要查阅参考手册的DMA请求映射表。

  2. 配置源地址:指向内存缓冲区首地址,设置MINC=0(存储器地址不递增,因为每次分发同一份数据)。

  3. 配置目标地址:指向第一个外设的数据寄存器,设置PINC=1(外设地址递增,实现跨外设分发)。

  4. 设置数据宽度:外设和存储器的数据宽度需要匹配(8/16/32位)。

  5. 设置传输数量:NDTR寄存器设置为 数据长度 × 外设数量。

  6. 使能传输完成中断:所有外设都收到数据后,DMA产生中断通知CPU。

代码示例(以STM32 HAL库风格示意):

(AI生成)

c

// 假设要发送同一组数据到3个SPI外设
// 内存数据:uint8_t txBuffer[256]
// 目标:SPI1->DR, SPI2->DR, SPI3->DR

DMA_HandleTypeDef hdma;

// 配置DMA:内存到外设,循环模式
hdma.Instance = DMA1_Stream0;
hdma.Init.Channel = DMA_CHANNEL_0;
hdma.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma.Init.PeriphInc = DMA_PINC_ENABLE;    // 外设地址递增
hdma.Init.MemInc = DMA_MINC_DISABLE;      // 内存地址不递增
hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma.Init.Mode = DMA_CIRCULAR;             // 循环模式
hdma.Init.Priority = DMA_PRIORITY_HIGH;

HAL_DMA_Init(&hdma);

关键点:STM32的DMA控制器通常一个通道只能服务一个外设。要实现“内存到多个外设”,需要利用DMA的“存储器到存储器”模式配合多个DMA通道,或者使用定时器触发不同通道的DMA请求,轮流将数据搬运到不同外设。这在STM32H7等高端型号的GPDMA上支持更好。

1.4 实际应用建议

  • 音频/I2S场景:用DMA双缓冲模式,CPU处理前一个缓冲区时,DMA填充后一个缓冲区,实现无缝音频播放。

  • ADC多通道扫描:DMA循环模式将ADC多通道结果自动搬运到内存数组,CPU零干预。

  • 注意Cache一致性:如果MCU有D-Cache(如Cortex-M7),DMA写入的内存区域需要手动失效Cache,否则CPU读到的是旧数据。

二、定时器带死区的互补PWM

2.1 第一性原理:为什么需要死区?

物理模型:想象一个半桥电路——两个MOSFET上下串联,中间点驱动电机。上管导通时,电流从电源正极流向电机;下管导通时,电流从电机流向地。如果两个管子同时导通,电源正极直接短路到地,瞬间产生巨大电流,管子炸掉。

数学描述:MOSFET不是理想开关。它有开通延迟(td(on)​)和关断延迟(td(off))。在关断过程中,VGS从高电平下降到阈值电压Vth​需要时间。在这个时间内,管子仍然导通。

死区时间 tdead 必须满足:

tdead>td(off)−td(on)

对于典型MOSFET,这个值在100ns到1μs之间。

PWM占空比与死区的关系:死区时间的插入会略微降低有效占空比,实际输出占空比为:

Deff=Dideal−tdeadTPWM

在低频PWM中,这个影响可以忽略;在高频(如100kHz以上)且高占空比场景下,需要补偿。

2.2 关键技术点

STM32高级定时器(如TIM1、TIM8)可以产生多达三个带死区插入的互补输出。配置时需要:

  1. 将通道及其互补通道对应的输出比较使能位(OCxE和OCxNE)置1。

  2. 在刹车与死区寄存器BDTR中设置DTG[7:0]位,计算死区时间。

死区时钟Tdtg的计算公式:

  • 如果DTG[7] = 0:Tdtg=TDTSTdtg​=TDTS​

  • 如果DTG[6] = 0:Tdtg=2×TDTSTdtg​=2×TDTS​

  • 如果DTG[5] = 0:Tdtg=8×TDTSTdtg​=8×TDTS​

  • 如果DTG[7:5] = 111:Tdtg=16×TDTSTdtg​=16×TDTS​

其中TDTS由时钟分频器CKD[1:0]决定。

死区时间计算:

  • DTG[7]=0DTG[7]=0:死区时间 = DTG[7:0]×TdtgDTG[7:0]×Tdtg​

  • DTG[6]=0DTG[6]=0:死区时间 = (64+DTG[5:0])×Tdtg(64+DTG[5:0])×Tdtg​

  • DTG[5]=0DTG[5]=0:死区时间 = (32+DTG[4:0])×Tdtg(32+DTG[4:0])×Tdtg​

2.3 代码示例

(AI生成)

c

// STM32高级定时器TIM1配置互补PWM带死区
TIM_HandleTypeDef htim1;
TIM_OC_InitTypeDef sConfigOC;
TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig;

// 时基配置:PWM频率 = 72MHz / (72 * 1000) = 1kHz
htim1.Init.Prescaler = 72 - 1;
htim1.Init.Period = 1000 - 1;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;

// PWM通道配置
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500;  // 50%占空比
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;

// 死区配置:约500ns
sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_ENABLE;
sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_ENABLE;
sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
sBreakDeadTimeConfig.DeadTime = 72;  // DTG值,对应约500ns
sBreakDeadTimeConfig.BreakState = TIM_BREAK_ENABLE;
sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE;

2.4 实际应用建议

  • 死区时间宁大勿小:先设置一个安全的较大死区(如1μs),用示波器观察波形确认无直通,再逐步减小。

  • 对于电机驱动,死区补偿可以改善低速时的转矩脉动。

  • 使用刹车输入(Break Input)连接过流保护信号,硬件级快速关断PWM。

三、系统节拍与RTOS任务调度

3.1 第一性原理:时间片是什么?

操作系统视角:单核CPU没有真正的“同时运行”。RTOS把CPU时间切成一个个小片,每个任务轮流使用。这个“小片”就是时间片,由硬件定时器产生的周期性中断(Tick中断)来驱动。

数学类比:这就像分蛋糕。假设每秒CPU有1000个时间片(Tick频率1kHz),3个同优先级任务各分1/3的时间片。每个任务在每个时间片内运行,时间片用完就被换下,下一个任务上场。

时间片轮转调度的效率取决于:

有效计算占比=1−tcontext_switchttick

其中tcontext_switch是上下文切换开销。在Cortex-M上,一次上下文切换大约需要1-2μs。如果Tick频率是1kHz(时间片1ms),开销占比约0.1-0.2%。

FreeRTOS的机制:FreeRTOS默认同时使用两种调度策略——抢占式调度(不同优先级之间,高优先级随时抢占)和时间片轮转(同优先级之间,每人轮流跑一个时间片)。

核心原则是优先级高于时间片:高优先级任务可以随时打断低优先级任务,不管低优先级任务的时间片有没有用完。

3.2 关键技术点

SysTick中断:每次SysTick中断(默认1ms)做两件事:tick计数+1,检查阻塞任务是否到期需要唤醒,然后判断是否需要触发任务切换。时间片轮转的判断逻辑就嵌在xTaskIncrementTick()函数中。

上下文切换:利用Cortex-M的PendSV异常实现。PendSV的优先级被设为最低,目的是让所有业务中断先处理完,再做任务切换,不影响中断实时性。

任务控制块(TCB) :每个任务有一个TCB,保存栈顶指针、栈起始地址、栈大小、优先级、任务状态等信息。上下文切换时,当前任务的寄存器值被压入其任务栈,然后从下一个任务的栈中恢复寄存器值。

3.3 代码示例:手动触发时间片轮转

(AI生成)

c

// FreeRTOS中时间片轮转的简化逻辑
void xTaskIncrementTick(void) {
    TickType_t xTickCount = 0;
    
    xTickCount++;  // 系统节拍递增
    
    // 检查延时列表中的任务是否到期
    if (xTickCount >= xNextTaskUnblockTime) {
        // 唤醒到期任务...
    }
    
    // 时间片轮转判断
    #if (configUSE_TIME_SLICING == 1)
    if (listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[pxCurrentTCB->uxPriority])) > 1) {
        // 同优先级就绪链表中有多个任务
        // 当前任务的时间片用完了
        taskYIELD();  // 触发任务切换
    }
    #endif
}

3.4 实际应用建议

  • Tick频率的取舍:太小→切换频繁,CPU开销大;太大→同优先级任务响应延迟增大。一般1kHz(1ms)是平衡点。

  • 延时必须用pdMS_TO_TICKS()宏换算,不要写死tick数,否则改变Tick频率后行为异常。

  • 空闲任务不可缺:所有用户任务阻塞时,CPU至少有一个任务可跑,同时空闲任务负责回收被删除任务的TCB和栈资源。

四、用示波器测电源完整性

4.1 第一性原理:你测到的真的是电源纹波吗?

物理模型:示波器探头和地线构成一个测量回路。这个回路有寄生电感Lloop​,与探头输入电容Cin​构成一个LC谐振电路。谐振频率为:

fresonance=12πLloop⋅Cin

一根15cm的鳄鱼夹地线,寄生电感约100-150nH。探头输入电容约10pF。谐振频率约4-5MHz。这意味着:电源轨上任何超过这个频率的噪声,测量回路自己会“制造”出额外的振铃,让你误以为电源纹波很大。

核心结论:你测到的“纹波” = 电源真实纹波 + 测量回路引入的伪影。

4.2 关键技术点

正确的测量方法:

  1. 使用短地弹簧:探头尖端接输出电容正端,接地弹簧接输出电容负端,把测量环路面积压到最小。地线长度控制在3cm以内。

  2. 使用AC耦合:切到AC耦合挡位,把几伏的直流分量挡掉,只放大看毫伏级交流纹波。但注意:AC耦合会屏蔽掉很慢的直流变化,测负载瞬态时可能需要DC耦合。

  3. 打开20MHz带宽限制:用来观察datasheet里常见的低频纹波指标。但如果要排查高频尖峰,就不能开带宽限制。

  4. 示波器平均模式:设置1024次平均,随机噪声被滤除,可以清晰辨别周期性纹波。

4.3 实际应用建议

  • 先验证测量方法再改电路:用长地夹测一次,保存波形;换成短地弹簧再测一次。如果高频尖峰明显缩小,说明原来的测量方式引入了很大误差。

  • 测开关电源纹波时,探头衰减比选择:10X探头会把信号衰减10倍,测几毫伏纹波时信号接近示波器噪声底。考虑使用1:1探头或专用Power Rail Probe。

  • 区分稳态纹波和负载瞬态:两者需要不同的示波器设置,不要混淆。

五、PID串级控制

5.1 第一性原理:为什么单回路PID不够?

控制理论视角:一个典型的工业过程(如锅炉温度控制)包含多个环节:燃料流量→燃烧→炉膛温度→传热→水温。这是一个大滞后、多变量耦合的系统。

用单回路PID控制水温:PID输出直接控制燃料阀门。问题在于——燃料流量变化后,炉膛温度变化需要时间,水温变化需要更长时间。在这段滞后时间内,PID还在根据旧的水温误差调整阀门,导致超调甚至振荡。

串级控制的数学描述:

单回路系统:Gplant(s)=G1(s)⋅G2(s)⋅G3(s),其中每个环节都有延迟。

串级系统:

  • 副回路:C2(s)⋅G2(s)C2​(s)⋅G2​(s),快速抑制进入副回路的干扰

  • 主回路:C1(s)⋅C2(s)G2(s)1+C2(s)G2(s)⋅G3(s)C1​(s)⋅1+C2​(s)G2​(s)C2​(s)G2​(s)​⋅G3​(s)

副回路把G2(s)这个“快环节”先稳定住,主回路面对的是一个“等效更快”的被控对象。

关键设计准则:副回路的时间常数应该是主回路的1/10以下,否则串级效果不明显。

5.2 串级PID的物理类比

把串级控制想象成公司管理:

  • 主回路(外环):CEO制定年度目标(水温目标),根据市场反馈(水温)调整部门目标(蒸汽压力目标)。

  • 副回路(内环):部门经理(蒸汽压力控制器)快速响应CEO的目标变化,直接管理一线员工(燃料阀门)。

CEO不需要每时每刻盯着每个员工,部门经理负责快速调节。CEO只关注整体战略。

5.3 代码示例:双回路串级PID

(AI生成)

c

// 串级PID结构体
typedef struct {
    float Kp, Ki, Kd;
    float integral;
    float prev_error;
    float output;
    float output_min, output_max;
} PID_Controller;

// 主回路(温度→压力目标):100ms周期
// 副回路(压力→阀门):10ms周期
// 主回路每10次副回路中断执行一次

volatile PID_Controller temp_pid;   // 主回路
volatile PID_Controller press_pid;  // 副回路

// 10ms定时器中断服务函数
void Timer0_ISR(void) {
    static uint16_t cnt = 0;
    
    // 1. 副回路:每次中断都执行(10ms周期)
    float press_actual = Read_Steam_Press();
    PID_Calculate(&press_pid, press_pid.target, press_actual);
    Set_Valve_Opening(press_pid.output);
    
    // 2. 主回路:每10次中断执行一次(100ms周期)
    if (++cnt >= 10) {
        cnt = 0;
        float temp_actual = Read_Water_Temp();
        PID_Calculate(&temp_pid, temp_pid.target, temp_actual);
        // 主PID的输出作为副PID的目标值
        press_pid.target = temp_pid.output;
    }
}

5.4 实际应用建议

  • 先整定副回路,再整定主回路:副回路控制器设为纯比例,从小到大增大Kp直到临界振荡,取临界增益的1/2作为工作增益。然后整定主回路。

  • 副回路通常用纯比例控制即可,主回路用PI或PID。

  • 异步采样:主回路采样周期是副回路的整数倍(如10倍),节省CPU资源。

六、寄存器位带操作

6.1 第一性原理:为什么要“位带”?

计算机体系结构视角:Cortex-M的存储器系统以字节为最小寻址单位。你不能直接操作一个地址的某一位。要设置GPIO的某个引脚,常规做法是:

c

GPIOA->ODR |= (1 << 1);   // 读-改-写三步操作

问题在于:这三步操作不是原子的。如果在读和写之间发生中断,且中断中修改了同一个寄存器,数据就会丢失。

位带操作的解决方案:Cortex-M3/M4内核开辟了一块位带别名区(Bit-band Alias),将位带区的每一个位映射到别名区的一个32位字。对别名区地址的读写,硬件自动转换为对位带区对应位的原子读-改-写操作。

映射关系(这是纯数学的地址变换):

  • SRAM区:AliasAddr = 0x22000000 + (A - 0x20000000) × 32 + n × 4

  • 外设区:AliasAddr = 0x42000000 + (A - 0x40000000) × 32 + n × 4

其中A是位带区字节地址,n是该字节内的位序号(0-7)。

为什么×32? 位带区1个字节 = 8位,映射到别名区8个字 = 32字节。所以地址增量是32倍。

6.2 代码实现

(AI生成)

c

// 位带操作宏定义
#define BITBAND_RAM(addr, bit) \
    (*((volatile uint32_t *)(0x22000000 + \
    (((uint32_t)&(addr) - 0x20000000) << 5) + ((bit) << 2))))

#define BITBAND_PERIPH(addr, bit) \
    (*((volatile uint32_t *)(0x42000000 + \
    (((uint32_t)&(addr) - 0x40000000) << 5) + ((bit) << 2))))

// 使用示例
// 原子地设置GPIOA的PA1引脚为高
BITBAND_PERIPH(GPIOA->ODR, 1) = 1;

// 原子地读取PA1引脚状态
uint8_t state = BITBAND_PERIPH(GPIOA->IDR, 1);

6.3 实际应用建议

  • 位带操作是Cortex-M3/M4的经典特性,但后续的Cortex-M处理器(如M7、M33)已不再支持。从软件兼容性角度,不建议在新项目中使用。

  • 现代替代方案:使用__atomic系列内建函数,或使用LDREX/STREX指令实现原子操作。

  • 位带操作最大的价值在于理解原子操作的本质——它教会你“为什么读-改-写不是原子的”以及“硬件如何提供原子性”。

七、芯片勘误表与编程手册

7.1 第一性原理:为什么芯片会有“错误”?

半导体物理视角:芯片设计是极其复杂的工程。一个现代MCU包含数千万甚至数亿个晶体管。在流片之前,设计验证覆盖率达到99.9%就已经是业界顶尖水平。剩下的0.1%意味着数千个潜在问题。

勘误表(Errata Sheet) 是芯片厂商对已知硬件缺陷的官方记录。它描述的是:芯片实际行为与数据手册描述之间的细微差异,以及建议的变通方法(Workaround) 。

关键认知:勘误表不是“可选阅读材料”,它是产品开发必读文档。一个典型的例子:某些STM32型号的DMA在特定条件下会丢失数据;某些ESP32型号的ADC在特定配置下精度不足。不了解这些,你的产品可能在客户现场才暴露问题。

7.2 关键技术点

勘误表的典型结构:

  1. 芯片版本标识:通过eFuse位或芯片丝印识别芯片的硅片版本(Revision)。

  2. 勘误摘要矩阵:列出每个勘误项影响哪些版本,快速判断你的芯片是否受影响。

  3. 详细描述:每个勘误项包含问题描述、影响、变通方法和参考代码。

编程手册(Programming Manual) :描述CPU内核、外设的编程模型(寄存器、指令集、异常模型)。参考手册(Reference Manual)描述具体外设的详细功能。数据手册(Datasheet)描述电气特性和引脚定义。

7.3 实际应用建议

  • 项目启动第一件事:下载并阅读目标芯片的勘误表,确认你的芯片版本和计划使用的功能是否有已知问题。

  • 建立勘误表检查清单:把与你项目相关的勘误项列出来,在代码审查时逐项检查。

  • 勘误表会随着芯片版本更新。如果更换了芯片批次,重新检查勘误表。

八、手写RTOS内核

8.1 第一性原理:从裸机到RTOS

裸机的问题:一个大循环while(1)里放了传感器采集、按键扫描、LED刷新。如果传感器采集需要等待200ms,这200ms内按键和LED全部冻结。

RTOS的解决方案:把每个功能封装成独立的任务,每个任务有自己的栈和优先级。调度器按规则分配CPU时间。任务等待时主动“睡觉”,把CPU让给其他任务。

调度器必须回答的四个问题:

  1. 有哪些任务在等着运行?(就绪队列的维护)

  2. 在这些任务里应该选哪一个?(调度算法)

  3. 被选中的任务怎么开始跑?(上下文切换)

  4. 什么时候重新做一次选择?(调度触发时机)

8.2 最小RTOS内核实现

任务控制块(TCB) :

(AI生成)

c

typedef struct task_control_block {
    void *stack_top;       // 任务栈顶指针
    void *stack_start;     // 栈起始位置
    uint32_t stack_size;   // 栈大小
    uint32_t priority;     // 优先级
    enum task_state state; // 任务状态
} TCB;

上下文切换的核心:利用Cortex-M的PendSV异常。PendSV优先级设为最低,确保所有中断处理完后才做任务切换。

(AI生成)

c

// 触发任务切换
void task_yield(void) {
    // 设置PendSV挂起位
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}

// PendSV中断服务函数(汇编实现)
__attribute__((naked)) void PendSV_Handler(void) {
    __asm volatile (
        "MRS R0, PSP\n"          // 获取当前任务栈指针
        "STMFD R0!, {R4-R11}\n"  // 保存寄存器
        "LDR R1, =current_tcb\n" // 保存栈顶到TCB
        "LDR R2, [R1]\n"
        "STR R0, [R2]\n"
        // 选择下一个任务...
        "LDMFD R0!, {R4-R11}\n"  // 恢复寄存器
        "MSR PSP, R0\n"
        "BX LR\n"
    );
}

SysTick驱动调度:SysTick中断中递增tick计数,检查延时任务,并调用task_yield()触发调度。

8.3 实际应用建议

  • 手写RTOS的核心价值在于理解机制,不是为了替代FreeRTOS。

  • 关键学习点:栈帧布局、PendSV切换、就绪队列管理、临界区保护。

  • 建议从最简单的“两个任务轮流跑”开始,逐步增加延时、优先级、信号量等机制。

九、掌握Simulink、LabVIEW、Visio

9.1 各自定位与第一性原理

工具核心用途第一性原理
Simulink模型在环(MIL)、自动代码生成用图形化方式求解微分方程,自动生成嵌入式C代码
LabVIEW数据采集、仪器控制、测试自动化数据流编程:节点在输入数据就绪时执行
Visio系统架构图、流程图、状态机图用标准符号表达系统结构和行为

9.2 应用建议

  • Simulink:用于PID控制算法的模型验证和自动代码生成。可以先在Simulink中仿真串级PID,验证参数后再自动生成C代码部署到MCU。

  • LabVIEW:用于产线测试,编写自动化测试脚本控制示波器、电源、信号发生器。

  • Visio:用于设计评审,画出系统状态机和架构图,团队沟通效率远高于文字描述。

十、状态机 + 中断 + DMA 实现零CPU占用传输

10.1 第一性原理:什么是“零CPU占用”?

严格来说,CPU不可能完全为0——初始化配置、中断处理都需要CPU。但数据传输本身的CPU占用可以趋近于0。

实现路径:定时器触发DMA,DMA搬运数据,DMA传输完成中断通知CPU做最少量的处理。

text

定时器溢出 ──▶ DMA请求 ──▶ 数据搬运(硬件) ──▶ 传输完成中断 ──▶ CPU仅做标志置位
    (硬件)        (硬件)         (硬件)              (中断)

物理类比:这就像自动灌溉系统。定时器是“闹钟”,到点就响;DMA是“水管”,闹钟一响就自动浇水;CPU是“园丁”,只在水浇完后过来检查一下,平时可以去做别的事。

10.2 状态机设计

(AI生成)

c

typedef enum {
    STATE_IDLE,
    STATE_RECEIVING,
    STATE_PROCESSING,
    STATE_TRANSMITTING,
    STATE_ERROR
} SystemState;

volatile SystemState sys_state = STATE_IDLE;
volatile uint8_t dma_complete_flag = 0;

// DMA传输完成中断
void DMA_TransferComplete_ISR(void) {
    dma_complete_flag = 1;
    sys_state = STATE_PROCESSING;
}

// 主循环中的状态机
void main_loop(void) {
    while (1) {
        switch (sys_state) {
            case STATE_IDLE:
                if (start_condition) {
                    sys_state = STATE_RECEIVING;
                    Start_DMA_Receive();
                }
                break;
                
            case STATE_PROCESSING:
                if (dma_complete_flag) {
                    dma_complete_flag = 0;
                    Process_Data();
                    sys_state = STATE_TRANSMITTING;
                    Start_DMA_Transmit();
                }
                break;
                
            case STATE_TRANSMITTING:
                if (dma_complete_flag) {
                    dma_complete_flag = 0;
                    sys_state = STATE_IDLE;
                }
                break;
        }
    }
}

10.3 实际应用建议

  • UART空闲中断 + DMA:这是最经典的“零CPU占用”接收方案。UART接收一帧数据后触发空闲中断,CPU处理数据,期间DMA自动搬运每个字节。

  • ADC定时触发 + DMA循环:定时器以固定频率触发ADC采样,DMA自动将结果搬运到内存数组。CPU只在数组满时处理。

综合应用案例:三相交错并联Buck变换器控制器

系统需求:设计一个数字电源控制器,驱动三相交错并联Buck变换器,输出电压12V,最大电流100A。

技术选型:

  • MCU:STM32G474(带高分辨率定时器HRTIM,适合数字电源)

  • 拓扑:三相交错,每相120°相位差

  • 控制:电压外环+电流内环串级PID

系统架构图:

text

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│ 12V输入  │────▶│ 三相Buck │────▶│ 输出滤波 │────▶│ 负载     │
│          │     │ (3×MOSFET)│     │ (LC)     │     │          │
└──────────┘     └────┬─────┘     └────┬─────┘     └──────────┘
                      │                 │
                 ┌────▼─────┐     ┌────▼─────┐
                 │ 3×驱动器  │     │ 电压/电流 │
                 │(带死区)   │     │ 采样电路  │
                 └────┬─────┘     └────┬─────┘
                      │                 │
                 ┌────▼─────────────────▼─────┐
                 │        STM32G474           │
                 │  ┌──────────────────────┐  │
                 │  │ HRTIM: 3对互补PWM    │  │
                 │  │ (120°相位差+死区)    │  │
                 │  ├──────────────────────┤  │
                 │  │ ADC: 电压/电流采样    │  │
                 │  │ (定时器触发+DMA)     │  │
                 │  ├──────────────────────┤  │
                 │  │ 串级PID: 电压→电流    │  │
                 │  ├──────────────────────┤  │
                 │  │ 状态机: 软启动/运行/  │  │
                 │  │ 保护 (过流/过压)      │  │
                 │  └──────────────────────┘  │
                 └────────────────────────────┘

工作流程:

  1. HRTIM产生三对120°相位差的互补PWM,死区时间约200ns

  2. ADC由HRTIM触发,在每个PWM周期中点采样电感电流和输出电压

  3. DMA自动将ADC结果搬运到内存数组

  4. 电流内环在每个PWM周期执行PID计算(约10μs周期),更新占空比

  5. 电压外环每100个PWM周期执行一次(约1ms),输出电流环目标值

  6. 状态机监控过流/过压事件,触发刹车输入硬件级关断

应用建议(总结)

  1. DMA优先:任何周期性数据传输都应该考虑DMA。CPU的时间应该花在“决策”上,而不是“搬运”上。

  2. 死区安全第一:功率电路调试时,先用大死区确认无直通,再逐步优化。

  3. 测量方法决定测量结果:测电源纹波前,先验证你的探头和接地方式。错误的测量方式比没有测量更危险。

  4. 勘误表是必读文档:项目启动时花30分钟读勘误表,可能节省30天的现场调试。

  5. RTOS不是万能药:简单的系统用状态机+中断就够了。引入RTOS会增加复杂性和调试难度。只有当系统确实需要多任务并发和优先级调度时,才使用RTOS。

  6. 串级PID的调试顺序:先内环后外环,先比例后积分再微分。内环调不好,外环再完美也没用。

  7. 代码复用来自理解:不要盲目复制粘贴代码。理解每一行代码背后的物理意义和数学原理,才能在出问题时快速定位。

  8. 工具服务于思想:Simulink、LabVIEW、Visio都是工具,核心是你对系统建模和控制理论的理解。工具只是把你的理解表达出来。

寒山转苍翠,秋水日潺湲。——王维《辋川闲居赠裴秀才迪》

嵌入式系统:嵌入式系统开发入门指南 这只是一个简单的嵌入式系统示例,嵌入式系统的开发涉及更多的概念和技术,如中断处理、设备驱动、通信接口、传感器集成等。如果您有更具体的问题或需要更深入的了解,请随时提问!嵌入式系统是一种特殊类型的计算机系统,它被设计用于执行特定的功能或任务。与通用计算机系统不同,嵌入式系统通常被嵌入到其他设备中,如家电、汽车、医疗设备、工业机械等。本文将介绍嵌入式系统的基本概念、特点和开发入门指南,并提供一些简单的源代码示例。以上示例演示了如何使用wiringPi库控制树莓派上的GPIO引脚,实现LED的闪烁效果。 阅读详情

相关推荐

1、嵌入式系统开发入门指南

本文是一篇嵌入式系统开发的入门指南,全面介绍了嵌入式系统的定义、分类、硬件组成以及软件开发流程。内容涵盖了嵌入式系统的三大主要分类:独立嵌入式系统、实时嵌入式系统(包括硬实时和软实时)以及网络嵌入式系统,并详细解析了嵌入式系统的硬件架构和软件开发方法。此外,还提供了基于Keil µVision3 C51和8051微控制器的实践实验室,帮助读者快速入门嵌入式开发。

smartcontract5的博客 75

探索嵌入式系统:快速入门指南概览

随着物联网、人工智能等技术的快速发展,嵌入式系统的应用领域将越来越广泛,掌握嵌入式系统开发技能将为我们带来更多的职业机会和发展空间。:安装适合嵌入式系统开发的IDE,如Keil、IAR Embedded Workbench等,这些IDE提供了编辑、编译、调试等功能,提高开发效率。:设计并实现一些简单的嵌入式系统项目,如LED闪烁、按键控制、串口通信等,逐步加深对嵌入式系统的理解。:理解电路的基本概念,如电压、电流、电阻、电容、电感等,并掌握简单的电路分析方法。

Srlua的博客 1340

1、 ARM嵌入式系统入门指南

本文详细介绍了ARM嵌入式系统的各个方面,包括ARM处理器核心、RISC设计哲学、硬件和软件组件、指令集特点以及具体应用场景。通过深入探讨操作系统、内存管理和优化技巧等内容,帮助开发者充分利用ARM处理器的高效、低功耗和高代码密度特性,开发出高性能的嵌入式应用。

weixin_30633869的博客 130

嵌入式系统入门学习1

嵌入式入门学习1:基于B站大学的课程学习,本文中提取课程知识点,适当的补充

weixin_50056251的博客 879

1、嵌入式系统与微控制器入门

本文介绍了嵌入式系统的基本概念、硬件分类及其特点,并重点探讨了微控制器在嵌入式系统开发中的应用。文章详细讲解了微控制器的硬件架构、编程语言和开发环境,包括STM32CubeIDE、Mbed和MicroPython等平台。此外,还通过具体实例展示了微控制器在数字输入输出、中断处理、定时操作、模拟-数字转换和数字通信等方面的实际应用,适合初学者和有经验的开发者参考学习。

play7的博客 66

8、嵌入式系统编程入门

本博客介绍了嵌入式系统编程的基础知识和技能,包括编程语言的选择、集成开发环境(IDE)的使用、仿真器和调试器的应用、编程工具的操作以及良好的工程实践。此外,还深入探讨了高级主题如代码优化、中断优先级管理、外设配置与通信接口等内容。通过丰富的示例代码和图表解析,帮助开发者更好地掌握嵌入式系统的开发过程并提升系统性能与可靠性。

yhn4567890的博客 131

嵌入式系统从入门到实战指南

定义:专用计算机系统,通常集成于更大的设备中常见应用领域:智能家居、工业控制、医疗设备、汽车电子核心特点:实时性、低功耗、资源受限。

jonevin的博客 310

嵌入式系统入门课件PPT完整版

嵌入式系统是以应用为中心、以计算机技术为基础,软硬件可裁剪,适应应用系统对功能、可靠性、成本、体积、功耗有严格要求的专用计算机系统。其与通用计算机的核心区别在于“专用性”——每个系统只为特定任务服务,如智能电表只负责电量计量与通信,而非运行Office或浏览器。典型的嵌入式系统具备三大核心特性:实时性:必须在规定时间内响应外部事件(如汽车ABS系统需毫秒级响应);资源受限:处理器性能、内存容量、功耗均有限制,常采用低功耗MCU;高可靠性。

weixin_30021053的博客 946

嵌入式系统开发:从入门到精通

定义嵌入式系统及其特点嵌入式系统与通用计算机系统的区别常见嵌入式系统的应用领域(如家电、汽车、医疗设备等)嵌入式系统的发展方向(AIoT、边缘计算)学习路径与职业规划建议鼓励动手实践与持续学习。

yxy15812441047的博客 352

华清远见嵌入式系统开发新手入门

本文还有配套的精品资源,点击获取 简介:嵌入式系统开发是计算机科学中的关键领域,涉及硬件与软件的紧密结合。本教程由华清远见提供,面向初学者,涵盖嵌入式Linux基础、C语言编程基础、硬件基础、驱动程序编写、系统移植、实时性处理和调试技巧等关键技能。通过学习本教程,初学者能够系统性地掌握嵌入式开发的基础知识和实践技能,为未来深入嵌入式系统开发奠定坚实基础。 1. 嵌入式...

weixin_33562004的博客 2206

树莓派嵌入式系统编程与Python入门

本文深入探讨了如何使用Python语言为树莓派编写嵌入式系统程序。首先介绍了编写嵌入式系统时必须使用的底层汇编语言和内核编程技巧,强调了与硬件通信的重要性。随后,文章详细解释了Linux操作系统在嵌入式系统中的应用,包括命令行操作、文本编辑器、用户账户管理以及脚本编程等。重点介绍了如何使用Python进行嵌入式系统编程,包括Python的基本语法、循环、条件语句、变量的使用以及如何通过Thonny Python IDE进行代码的编写与执行。

weixin_35751194的博客 363

嵌入式系统快速入门指南

定义与基本概念嵌入式系统与通用计算机的区别常见的嵌入式系统应用场景。

weixin_40770816的博客 230

Unity Shader实战:用Matcap给你的低模角色快速换上‘次世代’材质(附完整Shader代码)

本文详细介绍了Unity中Matcap技术的实战应用,通过Shader代码实现低模角色的次世代材质表现。Matcap技术利用预烘焙光照贴图,显著提升材质质感同时保持极低性能消耗,特别适合移动游戏和独立开发。文章包含完整Shader代码、平面模型着色优化方案及动态环境反射增强技巧,帮助开发者快速提升低模视觉效果。

weixin_28718755的博客 385

嵌入式系统入门:从基础到实战

嵌入式系统的基本定义与核心特征嵌入式系统与通用计算机系统的区别常见嵌入式系统的应用领域(工业控制、消费电子、医疗设备等)

2509_94235993的博客 456

LwIP在国产MCU的落地实践:从协议栈优化到网络稳定性实战

本文详细介绍了LwIP协议栈在国产GD32F407 MCU上的移植与优化实践,涵盖硬件平台选择、驱动适配、协议栈调优和网络稳定性提升策略。通过内存管理优化、实时操作系统集成和性能测试,为工业物联网应用提供高可靠性网络通信解决方案,显著提升数据传输效率和系统稳定性。

m2n3b4v5c6的博客 268

SAP 业务技术平台(BTP) Workflow(工作流)功能介绍

这背后其实发生了很多事情:Workflow 应用里 Determine Equipment Service Task 得到执行,通过 HTTP POST,消费部署在 SAP BTP 上的 Business Rules Service,后者根据我们在 Start New Instance 里传入的 Sample Data,返回自动决定出的设备明细。关于这个 SAP BTP 开发工具的更多介绍,参考我的文章:SAP 新一代全栈开发工具:SAP Business Application Studio.

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 8752

嵌入式系统入门全指南

定义嵌入式系统及其特点常见应用领域(消费电子、工业控制、医疗设备等)与通用计算机系统的区别。

qinqq0820的博客 195

cadence17.4和16.6同时安装无法使用的问题

经过对报错的问题分析,发现还是17.4的lic文件和16.6的不一样,16.6的版本总是无法找到自己对应的lic文件。卸载了,重新单独安装16.6,这里就是正确的。包括使用这些工具去修复这些问题,都无法接近。已经放弃了,使用哪个版本就安装 哪个版本。这个也无法解决问题。

D_Katter的博客 2321

专家推荐的几本入门嵌入式系统开发的好书

[原]专家推荐的几本入门嵌入式系统开发的好书 2007-10-16阅读577 评论0 1.>     作者:杜春雷  出版社:清华大学出版社  ISBN书号:7-302-06224-2  出版日期:2003年2月  本书描述:本书应该是做ARM 开发的一本基础教材,其中讲解了ARM编程模型,ARM汇编等一些与实际开发联系比较紧密的知识,这些知识对以后 b

jw903的专栏 3635

【RTD】AD7793两线式铂电阻PT100/PT1000应用

两线式RTD是低成本的检测方案,适用于检测精度不高、导线长度较短的场合。

只要思想不滑坡,想法总比问题多。 4255

ardupilot SBUS输入捕获

本节主要记录ardupilot 的sbus 输入捕获功能。ardupilot 默认支持多种无线协议控制输入设备,比如常用的PPM,SBUS,IBUS,DSM,SUMD,SRXL等协议,本节主要讲解ardupilot 如何通过定时器的pwm输入捕获来获取sbus数据。

陌城烟雨 269
上一篇: 一堂嵌入式内存课
CYY1921
博客等级 码龄1年 3粉丝 16原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CYY1921

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值