嵌入式进阶:启动流程、故障定位与OTA升级实战指南

开发者福利!热门AI工具限时免费用 购周边即赠Coding Plan Lite,Claude Code、Cursor等20+工具畅享,效率翻倍! 阅读详情

做嵌入式开发这些年,我越来越觉得有三块硬骨头是绕不过去的: 启动流程 故障定位 OTA升级 。它们不像写驱动那样有明确的对错,也不像调协议那样有抓包工具兜底,一旦出问题,往往是系统级别的“黑屏”或“死机”,排查起来全靠对底层机制的理解。这也是我在CSDN开这个付费专栏的初衷——把这三块内容系统地讲透,而不是零散地搜一篇看一篇。标题里的“启动流程深度拆解、故障定位方法论、OTA升级工程化实战”其实是同一根主线上的三个环节:先把系统看明白,再知道怎么找问题,最后才敢动升级。这篇文章我会把专栏设计的思路、启动流程的核心细节、故障定位的方法论框架、OTA工程化的关键决策,以及上篇课后思考题的完整解析,一次性讲清楚。无论你是刚接触RT-Thread的MCU开发者,还是正准备从单片机转向SoC平台的工程师,这篇文章都能帮你建立起一套完整的认知框架。

1. 专栏整体设计:为什么把启动、定位、OTA放在一起讲

1.1 三个主题的内在逻辑

你可能会问,这三个主题看起来是独立的,为什么要放在一个专栏里?我最初在设计课程大纲时也纠结过。后来在一次实际项目中被反复折腾后才想明白:启动流程是“系统从哪里来”的底层认知,故障定位是“系统为什么坏”的分析方法,OTA则是“系统怎么更新”的工程能力。三者环环相扣。

举个真实例子。某次产品量产前,客户反馈设备偶尔无法远程升级,复位后固件版本还是旧的。我第一反应是检查升级流程,后来排查发现是Bootloader跳转App的地址不对,升级标志位根本没被正常校验——这本质上是启动流程的锅。如果没有把启动流程吃透,你大概率会在OTA的代码里翻半天,方向就错了。

所以专栏的设计思路是:先用两讲把MCU和SoC的启动流程彻底拆开,建立“复位后每一行代码为什么执行”的底层直觉;再用两讲讲故障定位的方法论,教你怎么用科学手段而不是瞎猜解决死机问题;最后用三讲落地OTA升级的工程化方案,把分区、校验、回滚、断点续传这些实战细节全部覆盖。上篇结束后的思考题,正是为了帮你验证第一阶段的掌握程度。

1.2 内容难度与面向人群

这个专栏的定位是“进阶”,默认你具备基础的C语言和单片机开发经验,知道怎么点亮LED、跑个串口打印。但我不假设你理解链接脚本、异常向量表、启动引导这些底层的概念。

专栏的内容在国内嵌入式社区属于中上偏硬核的难度。我举一个具体的例子来说明:在讲启动流程时,我不只讲“Keil里勾选Use MicroLIB就能跑printf”,而是会把启动文件里 Reset_Handler 是怎么一步步调用 SystemInit __main main 的汇编逻辑讲清楚。这些内容对新手来说有点吃力,但对工作两年以上、想在技术上继续往深处走的工程师来说,正是最需要补的那块短板。

如果你目前还是纯应用层开发、对硬件寄存器也不太熟,建议先把 Cortex-M 权威指南的异常章节和RT-Thread的 board_init 源码过一遍再来学习,会顺畅很多。

2. 启动流程深度拆解:从复位向量到RT-Thread调度器启动

2.1 MCU启动:理解复位向量与启动文件的执行顺序

很多工程师对启动流程的理解停留在“上电后自动进main”这个层面,但这中间发生的事非常多。以Cortex-M为例,芯片上电后CPU从向量表的起始地址读取两个关键值:初始栈顶地址(MSP)和复位向量地址。这两个值分别放在向量表的偏移0和偏移4处,然后CPU跳转到复位向量指向的地址开始执行。

我建议你打开任何一个STM32工程的 startup_stm32f10x_hd.s 文件,仔细看 Reset_Handler 的代码。它做的事情是这样的:

Reset_Handler   PROC
                EXPORT  Reset_Handler [WEAK]
                IMPORT  SystemInit
                IMPORT  __main
                LDR     R0, =SystemInit
                BLX     R0
                LDR     R0, =__main
                BX      R0
                ENDP

这段汇编的逻辑很直白:先跳转到 SystemInit ,完成时钟配置;然后跳转到 __main ,这是C库的初始化入口,它会完成数据段的搬运、BSS段的清零,最后才调用你的 main 函数。

这其中容易被忽略的一个点是 [WEAK] 标志。它表示这个符号是弱定义的,如果你在工程里自己实现了 SystemInit ,链接时就会使用你的实现,否则使用启动文件里的默认实现。很多人在移植过程中发现时钟配置没生效,有时候就是因为弱符号和强符号的链接优先级没搞清楚。

2.2 RT-Thread的启动初始化流程

如果你用的是RT-Thread操作系统,启动路径会比裸机复杂一个层次。RT-Thread有两种启动方式:一种是 $Sub$$main 这种编译器钩子方式,在进入main之前先执行RT-Thread的初始化;另一种是传统的 rtthread_startup 显式调用方式。

以标准版的启动流程为例,典型调用链是:

void rtthread_startup(void)
{
    rt_hw_interrupt_disable();

    rt_hw_board_init();      // 板级初始化:时钟、内存、串口
    rt_show_version();
    rt_system_timer_init();  // 定时器初始化

    rt_system_scheduler_init(); // 调度器初始化

    rt_application_init();   // 创建main线程
    rt_system_timer_thread_init(); // 定时器线程
    rt_thread_idle_init();    // 空闲线程

    rt_system_scheduler_start(); // 启动调度器,不再返回
}

这中间有严格依赖关系的几个点: rt_hw_board_init 必须在 rt_system_timer_init 之前,因为系统定时器需要依赖板级初始化完成的心跳; rt_system_scheduler_init 要在创建任何线程之前,因为线程创建时会申请控制块并挂到就绪队列;而 rt_system_scheduler_start 一旦调用,就再也不会返回到 rtthread_startup 的调用者了。

我见过不少朋友在移植RT-Thread时遇到“串口没输出”的问题,第一反应是驱动坏了,其实很多时候是 rt_hw_board_init 里依赖的时钟树配置有问题,芯片的串口外设时钟没开。这里我的建议是:调试启动问题时,别急着看应用代码,先确认三个事实——串口引脚复用是否配置、外设时钟是否使能、波特率分频是否正确。这三步足够解决90%的串口启动打印异常。

2.3 SoC启动:BootROM、SPL、U-Boot的分层引导

如果你把视野从MCU扩展到SoC平台,比如常见的Cortex-A系列,启动流程会从“单级跳转”变成“多级接力”。我以典型Linux设备为例梳理一下:

第一级是芯片内部的BootROM。它固化在芯片硅片上,上电后自动执行,负责从你配置的启动介质(eMMC、SD卡、SPI Flash、USB等)读取下一级引导程序。BootROM的代码用户改不了,能改的是启动介质选择引脚,这些引脚的电平组合决定了BootROM去哪个外设找代码。

第二级是SPL(Secondary Program Loader)。这是一个小型的引导程序,相当于U-Boot的精简版,主要作用是初始化DDR内存、时钟等基础外设,然后把完整的U-Boot加载到内存中运行。为什么需要SPL?因为BootROM本身很小,通常只有几十KB的固件空间,没法完成复杂的内存初始化,所以需要SPL做“中间商”。

第三级就是完整的U-Boot了。U-Boot启动后,会加载设备树文件(dtb)和内核镜像(kernel),然后跳转到内核入口。这里有个关键细节:U-Boot在跳转前会设置好CPU寄存器和内存布局,特别是把机器ID或设备树地址放到指定寄存器中,让内核启动时能识别硬件配置。

U-Boot的启动日志是排查问题的金矿。你平时看到的 U-Boot SPL 2021.04 提示、 Loading U-Boot from MMC switch to partitions 这些信息,每一行都对应一个阶段的加载结果。如果卡在某一行不动,问题就能锁定到对应的外设或镜像区域。

2.4 MCU和SoC启动流程对比:一张表看懂差异

我把MCU和SoC的启动做一个系统性对比,方便你建立全局视角:

对比维度 MCU(以Cortex-M为例) SoC(以Cortex-A为例)
引导程序存放 片内Flash,向量表直接映射 外部存储介质,BootROM引导加载
第一级启动 复位向量跳转Reset_Handler 芯片内部BootROM
中间引导层 一般不需要 SPL、U-Boot
内存初始化 启动文件中可选配置 SPL/U-Boot中显式初始化DDR
操作系统加载 镜像直接烧录,无内核概念 需要加载设备树、内核镜像
启动时间量级 几百毫秒到秒级 秒到几十秒
调试手段 仿真器、SWD断点 串口日志、JTAG、Trace工具

这两者的核心差异在于:MCU是“单程序模型”,上电后执行的就是用户固件;SoC是“多阶段接力模型”,每一级引导程序负责初始化一部分硬件,最后才把控制权交到操作系统手里。理解这个差异后,你再去看Linux启动卡住的问题,就不会一头雾水地去猜了,而是会逐层检查:BootROM是否完成、SPL是否运行、U-Boot是否加载了正确的环境变量。

3. 故障定位方法论:用科学手段取代瞎猜

3.1 硬故障定位:从HardFault_Handler入手

嵌入式系统最常见的崩溃方式是HardFault。它的本质是CPU执行了非法操作,比如访问了不存在的地址、执行了未对齐的指令、或者除数为零。很多工程师遇到HardFault的第一反应是“加打印”,但往往崩溃现场的打印根本来不及输出。

正确的方法是:在HardFault_Handler里提取发生异常时的现场寄存器。Cortex-M内核在异常发生时,硬件会自动把一部分寄存器压栈,其中包括R0-R3、R12、LR、PC和xPSR。我们可以写一个固定的异常捕获函数来保存这些信息:

void HardFault_Handler(void)
{
    __asm volatile(
        "TST LR, #4\n"
        "ITE EQ\n"
        "MRSEQ R0, MSP\n"
        "MRSNE R0, PSP\n"
        "B hard_fault_handler_c\n"
    );
}

void hard_fault_handler_c(unsigned int *stack)
{
    unsigned int pc = stack[6];
    unsigned int lr = stack[5];
    unsigned int psr = stack[7];
    // 打印或存储PC/LR地址
}

这里的关键逻辑是 TST LR, #4 :通过判断LR寄存器的bit2,确定压栈用的是MSP还是PSP,从而拿到正确的栈指针。拿到 PC 值之后,你用编译生成的 .map 文件或 addr2line 工具反查,就能精确知道程序跑飞到了哪个函数哪一行,定位效率比瞎猜高出一个数量级。

3.2 死机与看门狗复位:别急着加狗

看门狗是嵌入式系统防死机的标配,但很多团队在使用上有个误区:代码跑飞了,看门狗把系统复位了,重启后又正常了,于是问题被掩盖。直到量产出现偶发复位,客户投诉了才知道这是个大坑。

我处理这类问题的原则是:看门狗只是最后的防线,不能作为故障定位的手段。遇到偶发复位,你先要排除是不是看门狗超时导致的复位。具体方法是在系统启动的最早阶段记录复位原因寄存器(比如STM32的 RCC->CSR ),并在日志中输出。这样你就能区分是“硬件复位”、“看门狗复位”还是“软复位”。

有一年我排查一个物联网设备的偶发死机,现象是设备运行几天后概率性重启。通过复位原因寄存器发现每次都是IWDG复位,于是把重点从“为什么死机”转到“程序在哪里卡住了”,最终定位到是低功耗模式下某个外设的中断标志没清,导致系统在休眠循环里反复触发中断,喂狗线程一直得不到调度。如果我当时不加复位原因记录,这个问题可能要排查两周。

3.3 故障定位方法论框架:复现、隔离、分析、验证

我总结的故障定位流程是四步法:复现、隔离、分析、验证。这四个步骤缺一不可。

复现是前提。如果问题无法稳定复现,那就需要加入更详细的日志、延长观测时间、甚至使用定时抓拍技术。隔离是核心。通过二分法逐步缩小排查范围,比如先判断是硬件还是软件、是中断还是主循环、是驱动还是协议栈。分析是动脑。把收集到的现场信息结合代码逻辑进行推演,形成假设。验证是闭环。针对假设做单点测试,确认修复有效后再进行压力验证。

这套方法听上去简单,但实际执行时最考验的是工程师的耐心。我见过太多人跳过了“隔离”直接进入“瞎改验证”,改一处测一次,运气好改对了,运气不好反而引入了新问题。所以我强烈建议:每次定位故障,先用笔在纸上写出“可能的假设列表”和“如何验证每个假设”,做完这一步再动手改代码。

4. OTA升级工程化实战:从能用升级到好用升级

4.1 分区规划:升级的地基工程

OTA升级绕不开分区设计。无论是MCU还是SoC平台,你都要在Flash里规划好Bootloader区、App区、下载缓存区和标志位区。以常见的MCU 1MB Flash为例,我通常这么规划:

分区 起始地址 大小 用途
Bootloader 0x08000000 64KB 启动引导、升级入口
App 0x08010000 800KB 应用固件
Download 0x080D0000 160KB 升级包临时存放
Flag 0x080FFC00 1KB 升级标志、版本信息

Bootloader放最前面,因为芯片上电后固定从0x08000000取向量表。App区要预留足够空间,不然最后一次升级版本变大就没法容纳了。Download区是一个容易被忽视的分区,如果没有它,你就只能“一边下载一边擦除App区”,一旦升级包下载了一半断了电,设备就会变砖。而有了Download区,升级包可以完整下载后再校验,校验通过才更新App区,安全性高很多。

这里有一个链接脚本层面的关键操作:App程序的链接地址必须改成App区的起始地址。以STM32为例,你需要在Keil的Target选项卡里把IROM1的Start改为0x08010000,Size改为0x000C8000。同时,App的向量表偏移也要在SystemInit之后重定向:

SCB->VTOR = 0x08010000; // 重定向向量表到App区起始地址

如果不做这一步,App里任意一个中断发生,CPU都会跳转到Bootloader的向量表,程序直接跑飞。

4.2 升级流程设计:断点续传、校验、回滚

成熟的OTA升级流程应该是这样的:设备从服务器下载升级包到Download区,边下载边计算CRC或哈希值;下载完成后做完整性校验;校验通过后设置升级标志,然后复位进入Bootloader;Bootloader检查升级标志,把Download区的固件拷贝到App区;拷贝完成后再次校验,成功则清除标志并跳转到新App,失败则回滚到旧App。

回滚机制的实现,关键是在Bootloader里保留一个“上次可用的App”区域,或者使用双Bank方案。双Bank的意思是Flash里有两个App区,分别是Bank A和Bank B。当前运行在Bank A,升级包就写进Bank B,写完后切换启动地址到Bank B。如果Bank B运行失败(比如连续复位几次),Bootloader就回切到Bank A。这种方案的优点是升级过程中App区始终有一个可用的系统,缺点是Flash占用翻倍,对资源紧张的MCU是个奢侈选择。

我在实际项目中更多采用“下载区+App区”方案,配合一个简单的“启动计数器”实现回滚:Bootloader中维护一个变量,记录新App的启动次数。App启动后若能正常运行超过某个阈值(比如30秒),就把标志置为“确认OK”;否则Bootloader在连续N次启动新App失败后,自动从备份区恢复旧版本。这个方法占用的资源较少,工程上非常实用。

4.3 OTA的可靠性设计:断电、Flash擦写异常都不怕

OTA最怕的情况是升级中途掉电。为了在掉电后恢复,Bootloader需要具备“升级元数据”持久化的能力。我建议单独划一个Flag区,里面记录升级状态机,包括:空闲、下载完成待升级、升级中、升级完成待确认。每次状态切换都要写Flash并同步做校验。

这里还有一个很多新手会踩的坑:Flash的写操作前必须先擦除,而擦除的最小单位是一个扇区(通常在4KB到64KB不等),不是按字节来的。如果你在升级过程中一边擦除一边写入,掉电后Flash可能处于“半擦半写”的中间状态,下次启动时读取的数据既不是新版本也不是旧版本,设备直接变砖。解决办法是在拷贝前先验证Download区的固件完整,再执行“先擦除App区、再逐扇区写入”的流程,整个过程禁止被中断打断。

在协议层面,我建议升级包加上序列号或者版本号校验。服务器下发时带上目标设备的硬件版本号,设备侧校验通过后才接收升级包。这样可以避免误把A型号设备的固件刷到B型号上,这类低级错误我见过不止一次。

5. 上篇课后思考题完整解析

5.1 思考题一:为什么RT-Thread启动时先关闭中断?

这道题考察的是对系统启动时序的理解。答案是:在资源初始化完成之前,任何中断都可能导致不可预测的操作。

RT-Thread在 rtthread_startup 的第一行就调用 rt_hw_interrupt_disable() ,这是为了防止在调度器和定时器尚未初始化时,外部中断触发导致系统进入未定义状态。比如串口中断在 rt_hw_board_init 前触发,此时中断处理函数可能还挂在默认的中断向量上,或者相关的设备结构体还没初始化,执行起来就会出错。

等系统完成初始化、创建了必要的线程后,再使能中断,此时中断处理函数才能安全地访问RT-Thread提供的服务接口。所以这个问题的标准回答是:保证系统初始化过程的原子性,避免中断破坏未完成的数据结构。

很多面试者会漏掉另一层细节:关闭中断的代价是中断响应延迟,所以在关键初始化路径之外,不应该长时间关中断。RT-Thread在 rt_system_scheduler_start 之前会重新使能中断,正是基于这个考量。

5.2 思考题二:MCU直接跳转到U-Boot需要满足什么条件?

这是一个跨体系的问题,考察你能不能把MCU和SoC的启动流程统一起来理解。如果在一个带Cortex-M的平台上跑一个简化版的U-Boot,跳转条件至少有三个方面:

第一,目标代码必须已经加载到正确的内存地址,并且该内存地址是可执行区域。如果代码在Flash里,要确保Flash等待周期和映射地址正确。第二,CPU必须处于所要求的工作模式,一般需要退出低功耗模式或切换到特权模式。第三,栈指针必须有效,否则跳转后的第一个函数调用就会崩溃。

这里我要补充一个工程细节:从Bootloader跳转到App时,有个通用的跳转代码框架,细节是先在跳转前关闭全局中断、关闭SysTick、把外设复位到默认状态,再读取App向量表的MSP和Reset_Handler地址,最后设置MSP并跳转。核心代码如下:

typedef void (*pFunction)(void);
pFunction jump_to_app;

void jump_to_application(uint32_t app_addr)
{
    uint32_t app_msp = *(volatile uint32_t *)app_addr;
    uint32_t app_reset = *(volatile uint32_t *)(app_addr + 4);

    __disable_irq();
    SCB->VTOR = app_addr;

    jump_to_app = (pFunction)app_reset;
    __set_MSP(app_msp);
    jump_to_app();
}

这个代码里最容易漏掉的是 SCB->VTOR = app_addr; 这一行。如果不重定向向量表,中断服务函数寻找入口时会从旧地址查找,导致跳转后第一次中断就死机。

5.3 思考题三:如何区分系统复位是看门狗触发还是外部引脚复位?

标准答案是通过读复位原因寄存器来判断。在STM32上,这个寄存器是 RCC->CSR ,其中bit29表示IWDG复位,bit28表示WWDG复位,bit26表示PIN复位,bit24表示POR/PDR复位。程序启动时第一时间读取该寄存器并记录下来,就能区分复位来源。

我实际工作中更建议把这个信息打印到日志里,并加上时间戳。这样每次复位后,你都能从历史日志中看出设备复位的频率和原因类型,对分析偶发问题帮助极大。

6. 常见问题与排查技巧实录

6.1 启动阶段问题速查表

现象 可能原因 排查手段
上电后完全无输出 时钟配置错误、串口引脚复用错误 检查SystemInit,确认调试器能连上
打印乱码 波特率不匹配、晶振频率配置错误 用示波器测TXD波形,核对分频系数
程序死在HardFault 中断未重定向、数组越界、栈溢出 抓现场PC值,配合map文件反查
RT-Thread启动后死循环 动态内存堆未初始化 确认 _Heap_Size 是否足够
U-Boot停在DRAM初始化 DDR参数不匹配 检查DDR颗粒型号和时序参数

这套速查表我建议你打印出来贴在工位旁边。启动阶段的问题往往不是单一原因,而是多个条件叠加,所以排查时要有顺序:先电源,再时钟,再串口,再中断,最后才看业务代码。

6.2 OTA升级失败排查清单

我整理了另一个Ota高频问题清单,都是项目群里问过多次的:

  • 升级包下载完成但校验失败:先确认Download区的写入地址是否越界,再确认固件生成时CRC是否和服务器端保持一致。
  • 升级成功后App起不来:优先检查向量表重定向是否执行,以及链接地址是否和实际Flash地址一致。
  • 升级到一半设备断电,重启后无法引导:这是分区规划的锅,说明缺少可靠的升级状态机,需要完善Flag区的持久化逻辑。
  • Bootloader能进但不能跳转App:检查App区的起始两个word是不是有效的MSP和Reset_Handler,或者App区根本没有有效固件。

针对最后一个问题,我再分享一个排查技巧:在Bootloader里增加一个“跳转前诊断”函数,读取目标地址的前8个字节并打印出来。如果打印的值不是0x2000xxxx开头的栈顶地址、0x0800xxxx开头的复位函数地址,那说明App区写入的数据本身就是错的,要么是固件没烧进去,要么是下载过程中数据损坏。这样你就能在5分钟内把问题从“系统级”缩小到“数据级”。

6.3 独家避坑经验

最后说几个我在项目中被坑过多次的经验,这些在文档里都查不到。

第一个是关于Bootloader和App的共享中断问题。如果Bootloader里使能了某个外设中断,跳转到App前没有完全关闭,那么App第一次执行时该中断可能立即触发,而App的中断向量表还没准备好,系统直接HardFault。我的习惯是在跳转前写一个 deinit_all_peripherals 函数,把用到的外设全部复位,宁可慢几毫秒也不能留隐患。

第二个是关于编译器优化对调试的影响。高优化级别下(比如O2),局部变量可能不实际存在于栈上,你在断点里看到的变量值可能是“过期”的。排查疑难问题时,建议先用O0编译来复现,确认问题与优化无关后再切换到发布优化等级。如果问题只在O2下出现,那几乎可以断定是时序或未定义行为导致的,比如未初始化变量、内存对齐错误、或者寄存器溢出。

第三个是关于Flash磨损和擦写次数的估算。OTA产品如果频繁升级,Flash的擦写寿命是必须计算的一项指标。比如一个扇区的擦写寿命是1万次,设备一个月升级两次,那么这个扇区只能用400多年——看起来够了,但如果你的日志系统也频繁写同一个Flash扇区,寿命会迅速消耗。设计时尽量让日志和OTA缓存落在不同扇区,同时把Flash的擦写次数纳入产品设计评审的检查项。

7. 写在专栏后面:我的一点实战体会

专栏更新到现在,我最大的体会是:嵌入式工程师很容易在“能用”和“好用”之间自我满足,但真正拉开差距的,是对底层机制的掌控力和问题定位的系统性思维。启动流程、故障定位、OTA升级这三件事看似零散,其实都是同一套底层功底的体现——你越理解系统是怎么启动的,越知道故障从哪里找起;你越能把故障定位的方法论用得熟练,越有信心去设计复杂的升级方案。

最后再分享一个小技巧:每次接手一个嵌入式项目,我都会花半天时间把工程里所有汇编启动文件、链接脚本和系统初始化代码通读一遍,并在关键位置加上注释。这个习惯看似浪费时间,但往往能让你在项目后期省下数倍的时间——因为你对系统的“默认状态”了如指掌,任何异常都逃不出你的排查框架。希望这篇专栏文章也能帮你建立起这样的框架,少走一些我当年走过的弯路。

c# 数字日期换为中文日期 有的项目中需要把数字日期换为中文日期,例如:2012-11-30 要求换为二○一二年十一月三十日 下面的类可以完成此功能(只能是“2012-2-24”或“2012/12/3”类型的日期字符串) 阅读详情

相关推荐

C#时间格式换为中文格式(后端)

在上述代码中,我们使用了一些自定义的格式化符号来表示中文日期和时间的格式。其中,"yyyy"表示四位数的年份,"M"表示月份(不带前导零),"d"表示日期(不带前导零),"HH"表示24小时制的小时数,"mm"表示分钟,"ss"表示秒。你可以根据自己的需求自定义时间格式化字符串,以满足特定的格式要求。当我们需要将时间格式换为中文格式时,可以使用以下方法来实现。使用上述代码,我们可以将当前时间格式化为类似于"2023年10月2日 15:30:45"的中文格式。运行上述代码,将会输出当前时间的中文格式。

code_welike的博客 810

C#数字日期装换为中文日期

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 namespace ConsoleApplication1 6 { 7 class Program 8 { 9 10 ...

awira24020的专栏 319

C#数字日期装换为中文日期(源码)

C#数字日期装换为中文日期,源码1037

C#代码直接显示中文星期几

C#代码直接显示中文星期几。

weixin_44858540的博客 365

C#将日期换中文格式显示

C# 将日期换成中文格式 没有什么难点,只是要小心,要考虑到月、日上 10 的说法,比如:10 不能直接换成一〇,也不能像上 20 那样换成一十〇,应该是...

MrLsss的博客 2044

C#数字日期中文日期

using System;using System.Collections.Generic;using System.Text;using System.Text.RegularExpressions;namespace ConvertDateToChinese{    class Convert    {        private static Convert instance=null; 

.NET 1978

日期换为中文大写数字

日期换为中文大写数字 动手写一个换日期的小方法,虽然很短,但是需要考虑的东西还是挺多的,记录一下。 /// <summary> /// 将日期换为中文大写 /// 如:一九八三 十一 二十七 /// &l...

G13327375540的博客 384

C# 将日期换成中文格式

没有什么难点,只是要小心,要考虑到月、日上 10 的说法,比如:10 不能直接换成一〇,也不能像上 20 那样换成一十〇,应该是十。 特点总结: 数字为 10 时,结果为十; 数字大于10 时,十位数字的中文加上“十”。 数字能被 10 整除时,个位数不报。 根据...

chongli0987的博客 783

c#版的阿拉伯数字中文大写,以及票据日期的写法

前接上篇,阿拉伯数字中文数字是蛮有意思的,最近又有新发现~现在更新一下以前的代码~ c#版的阿拉伯数字中文大写,以及票据日期的写法 不废话,直接上代码,两个方法 阿拉伯数字中文: private static char[] CN_UPPER_NUM = { '零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖' }; pri

qqtt789632147的专栏 1024

c#把日期改成数字字符串_C#编写壹个函数将输入的中文日期换为阿拉伯数字日期...

using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace DateConversion{class Program{static void Main(string[] args){//案例:编写一个函数进行日期换,将输入的中文日期换为阿拉伯数字日期,比如:二零一二年十二...

weixin_39784195的博客 1175

C# 日期换为中文大写

/// <summary> /// 日期换为中文大写 /// </summary> public class UpperConvert { public UpperConvert() { // // TODO: 在此处添加构造函数逻辑 // } //把数字...

dianjin3567的博客 405

c# 利用正则数字日期为汉字日期

最近再写一个C#的项目,需输出汉字日期,网上翻了一圈发现写的都比较麻烦。 所以结合网上将数字换为汉字大写金额的正则,写了个日期换函数。 public static String ConvertToChineseLite(Decimal number) { //将数字化为汉字 var s = number.ToString(...

pyrogas的博客 610

C#中把货币、日期换成中文大写

日期换代码如下: /**////<summary> ///日期换为中文大写 ///</summary> publicclassUpperConvert { publicUpperConvert() { // //TODO:在此处添加构造函数逻辑 // } ...

weixin_30894583的博客 108

将大写数字的日期换为阿拉伯数字的方法

    前两天在公司举行的编程比赛,做了一道题。如下:         编写一个函数进行日期换,将输入的中文日期换为阿拉伯数字日期,比如:二零零八年五月八日要换为2008-05-08。    因为觉得有趣,而且有共通的地方。比如说将大写的现金金额换成阿拉伯数字。故特扔出来抛砖引玉。        我用的是C#.                  private string

Jack_He的专栏 2253

C# DateTime:日期、日期差、时间时间

c#中如何获取日期 今天 DateTime.Now.Date.ToShortDateString(); 昨天,就是今天的日期减一 DateTime.Now.AddDays(-1).ToShortDateString(); 明天,同理,加一 DateTime.Now.AddDays(1).ToShortDateString(); 本周(要知道本周的第一天就得先知道今天是星期几,从而得知本周的第一天就是几天前的那一天,要注意的是这里的每一周是从周日始至周六止 DateTime.Now.AddDays(Co

weixin_40948750的博客 7953

java 阿拉伯数字日期换为中文大写日期方法_日期换为中文大写数字

/// ///将日期换为中文大写///如:一九八三 十一 二十七/// public classChineseNumberHelper{static Dictionary _theNumOfChineseCapital = new Dictionary(){{0,"〇"},{1,"一"},{2,"二"},{3,"三"},{4,"四"},{5,"五"},{6,"六"},{7,"七"},{8,"八"...

weixin_42512012的博客 940

C#数字日期成中文日期

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string[] args...

weixin_30809173的博客 441
上一篇: Flutter集成端侧TTS:sherpa-onnx+ZipVoice实现离线声音克隆
下一篇: 压缩包处理实战:从文件名解读、解压排错到安全归档
weixin_34060741
博客等级 码龄11年 4151粉丝 909原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值