使用C语言进行嵌入式开发
C语言语法简单、容易上手,因此常被各大高校用于编程教学或与电子信息专业结合作为单片机的主要开发语言。通常的编程是指在桌面级平台、服务器平台、PC或其他通用计算机上进行程序编写,在现代的编程环境下,C常常被诟病“老套、不美观、面向过程、难以使用”,但事实上C11标准引入以来,Linux生态中C语言扮演着重要角色。同时,在更广阔的嵌入式开发领域,C语言作为一个有着靠近底层、高效率,而又不依靠汇编实现的编程语言发挥着重要作用
以上都是废话
这篇博文主要介绍使用C语言进行嵌入式开发的一些基础要点,并简要介绍常用的工具
本篇建立在前两篇《现代C语言》的基础上,不再介绍gcc、make、cmake、kconfig等工具
交叉编译器与编译工具
开发嵌入式程序最重要的就是使用交叉编译器和支持对应硬件平台的IDE。嵌入式平台往往无法运行完整的代码编辑器、编译器,所以必须使用交叉编译器在PC端对本地写好的程序进行编译,然后将生成的.hex、.bin、.elf二进制文件烧录到嵌入式平台,在这个过程中常用如下的工具
arm-gcc
arm-gcc是编译ARM指令集程序gcc的统称。对于GNU交叉编译器(GCC面向交叉编译的成果)来说会采用如下命名格式:
<arch>-<vendor>-<os>-<(gnu)eabi>
arch:CPU架构、指令集
vendor:工具链提供商
os:目标操作系统
eabi:是否支持嵌入式应用二进制接口EABI
作为例子,arm-gcc下有几个主要软件:
arm-none-linux-gnueabi-gcc
即ARM architecture,no vendor,creates binaries that run on the Linux operating system,uses the GNU EABI
用于基于ARM架构的Linux系统,用于编译ARM架构的uboot、Linux内核、在Linux系统上运行的应用程序。
它基于gcc,使用Glibc库,EABI也是基于GNU支持的。
提到GNU,那懂得都懂,喜闻乐见的GPL猎杀时刻,编译出来的东西都要带GPL啦!
需要提以下EABI和ABI的概念:ABI即二进制应用程序接口(Application Binary Interface)。ABI描述了应用程序(或者其他类型程序)和操作系统之间或其他应用程序之间的低级接口。
EABI即嵌入式ABI(Embedded Application Binary Interface)。它指定了文件格式、数据类型、使用、堆组织优化和嵌入式软件中重要参数的标准约定。EABI是在ABI概念发展之后出现的,相当于ABI的一个特化版,支持软件浮点和硬件浮点功能混用,系统调用的效率更高,和今后的工具更兼容,并且软件浮点情况下,EABI的软件浮点的效率要比OABI(老式的ABI)高很多
arm-linux-gcc
即ARM architecture,creates binaries that run on the Linux operating system
最基本的ARM平台上Linux开发的gcc
arm-none-eabi-gcc
这个gcc是没有操作系统设备使用的,不支持跟操作系统关系密切的函数,POSIX的pthread是别想了。并且这个编译器使用的是newlib这个专用于嵌入式系统的C库。写嵌入式软件的人最常打交道的编译器应该就是它了,几乎所有ARM设备都可以用它写程序
stm32的CubeIDE就是使用了这个软件作为编译器,如果要进行底层的linux移植也要使用它编译linux源码
arm-eabi-gcc
用于编译安卓程序的gcc,如果要编写安卓程序那就离不开他了
EIDE
EIDE即Embedded IDE,是一款基于vscode开发的小型嵌入式平台IDE,能够实现对8051、arm、risc-v、stm8等平台进行编程
在vscode的插件中搜索Embedded IDE,再根据官方文档的操作步骤就可以实现部署
特别方便的是EIDE支持一键配环境,这样就不用费事去官网找SDCC、gcc-arm等编译器安装了,同时也会一并把openocd或flashtools安装好,支持直接烧录和调试
Keil
经典的嵌入式编程IDE,只要写各种32位MCU就逃不开的东西(写51也常用)
缺点一大堆,唯一的优点就是调试功能强大。几乎所有上古IDE的缺点它都占全了,但是就他能调试(EIDE那种拉跨调试还是算了吧),属于是市场垄断了。
riscv-gcc
随着RISC-V指令集出现的GCC,使用下面的指令安装RISC-V工具链
1 | git clone --recursive https://github.com/riscv/riscv-gnu-toolchain |
里面的一个子仓库就包含了riscv-gcc工具
专门用来给risc-v架构的设备做交叉编译,主要分为下面几个版本:
riscv32-unknown-elf-gcc
针对于RV32架构的编译器,使用newlib
可以添加参数来支持指令子集
1
--with-arch=rv64imc
支持裸机,不支持RISC-V平台的Linux
riscv64-unknown-elf-gcc
RV64版本的编译器,特性同上
riscv-none-embed-gcc
专用于riscv嵌入式交叉编译的gcc,
riscv32-unknown-linux-gnu-gcc
支持32位Linux的riscv-gcc
使用GLibc库
riscv64-unknown-linux-gnu-gcc
同上,支持64位Linux的riscv-gcc
riscv64-multilib-elf-gcc
在裸机编译riscv工具链的时候可以选择编译multilib版本的gcc
同时支持32位和64位
riscv64-liunx-multilib-gcc
同上,但是用于Linux下的程序编译
ST-CubeIDE
意法半导体专用于本家STM32的IDE,基于Eclipse开发,使用arm-none-eabi-gcc
作为编译器,使用gdb
作为调试器,使用openoc
作为烧录器
使用体验要比keil好很多,至少支持代码高亮和跳转更舒服了
串口工具
Minicom简介
minicom是在linux环境下常使用的串口工具,它提供了命令行中访问串口的功能
安装(ubuntu为例)
1 | sudo apt install minicom |
基本操作如下:
1 | minicom -s # 进入设置 |
在软件中先按【Ctrl】 +【a】,再按【z】即可进入设置模式,再按【x】可退出
进入设置模式后可以按上面显示的按键操作进入对应的选项
按【o】可进入串口的具体设置,包括调节波特率、选择端口都可以在这里完成
minicom默认读取/dev/modem
,如果提示找不到,可以先使用下面的指令创建软链接到/dev/modem
1 | ln -s /dev/modem 你接入串口对应的文件 |
或先使用参数进入minicom的设置界面,更改要读取的文件并保存后再正式进入软件
Putty简介
Putty是一个多平台兼容的虚拟命令行软件
可以支持SSH、串口终端、Telnet协议、Raw数据等登陆方式
有Win、Linux、MacOS多操作系统支持
配合百度就可以轻松使用,算是非常常用的终端软件了(如果不喜欢它也可以试试XShell)
调试与OpenOCD
以STM32为例,整套调试系统的架构如下所示
主要包含以下几个部分:
- 片上JTAG接口:位于MCU内部的JTAG接口电路,与CPU的调试组件相连,以状态机的形式提供对MCU的调试功能
- JTAG转接器:又称为调试器/仿真器或硬件加密狗,提供USB到JTAG协议的转换功能,也可以提供基于硬件的仿真功能
- 调试器驱动:为上位机操作系统提供的调试器USB驱动,用于上层软件通过操作系统对硬件仿真器进行控制
- 调试器软件:主要包含各种IDE里面闭源的调试器软件和开源的OpenOCD,提供一个GDB服务器,接收GDB客户端发送的指令,同时也兼容基于Telnet或各种私有协议(比如远程调试协议)的上位机连接。对仿真器数据的解码工作也在该软件内部完成
- GDB:一个C调试软件,GNU DeBugger,用于提供基于二进制文件的断点调试、内存查看等功能。各种IDE中的断点、寄存器查看、变量显示等功能都是基于GDB完成的,它作为GDB客户端发送指令给下位机
- Tcl/Python脚本:可用于OpenOCD执行的脚本文件,常用于烧录、清除FLASH等
JTAG接口协议
JTAG(Joint Test Action Group,联合测试工作组)是一种国际通用的标准化芯片内部测试协议,现在大多数可编程器件都支持JTAG协议,通用处理器、DSP设备、FPGA器件、大多数MCU-SoC等
标准JTAG是4数据线:
- TMS:测试模式选择(Test Mode Switch),用来设置JTAG接口处于某种特定的测试模式
- TCK:测试时钟输入(Test Clock),用于设置JTAG总线时钟
- TDI:测试数据输入(Test Data In),数据通过TDI引脚输入JTAG接口
- TDO:测试数据输出(Test Data Out),数据通过TDO引脚从JTAG接口输出
由于JTAG在定义初期,计算机设备都使用并口传输数据,因此它保留了2*8并口的标准,现在能看到的牛角座接入JTAG标准也是从并口演变而来的,只不过把一些并口数据线设为接地,一些数据线设为空脚
在传统的并口JTAG中,存在TRST、TDI、TMS、TCLK、TDO、RTCK、RESET这些数据线,因此比较冗余,在要求体积的设备上不推荐使用,但好处在于市面上大部分JTAG调试器都默认支持这个协议
为了支持JTAG协议,需要现在器件内部定义一个TAP(Test Access Port,测试访问口),并在CPU内核里设置专门的调试指令。TAP是一个通用的端口,要求通过使用TAP访问芯片中所有数据寄存器和指令寄存器,整个过程通过独立的TAP控制器完成,这个控制器一般使用较为复杂的状态机实现,独立于CPU内核,甚至CPU的通用寄存器都可以由它访问,因此TAP控制器一般被称为TAP状态机。
除了调试,JTAG还可以进行ISP在线编程;多个器件可以通过JTAG接口以菊花链的形式链接到一起,实现对多个器件分别测试
SWD与JTAG
SWD是ST公司在JTAG之外开发的独立的调试总线,即串行调试线(Serial Wire Debug),它只需要2条数据线即可实现高速调试:
- SWDIO:串行调试数据线
- SWCLK:SWD时钟数据线
JLINK与ST-LINK
J-Link是德国SEGGER公司推出基于JTAG的仿真器,其实就是一个USB转JTAG的适配器,但是它做到了跨平台、高速高效、稳定性极强,配合上位机调试软件可以实现最高速的调试
ST-LINK是ST公司专门针对STM8和STM32系列芯片推出的仿真器。ST-LINK/V2可指定使用SWIM接口或JTAG或SWD标准接口对设备进行调试
JTAG片上硬件调试原理
JTAG片上设备,也就是TAP状态机,需要实现以下数据线:
- TCK:TAP总线时钟。由于JTAG总线时钟是独立的,TAP时钟不会由片内总线提供,而是由片外的JTAG引脚接入
- TMS:模式选择信号,用于控制TAP状态机的转换。
- TDI:数据输入信号。
- TDO:数据输出信号。由上面两个数据线实现全双工数据传输
- TRST:复位信号,用来对TAP状态机进行复位(初始化)。这个信号接口在IEEE 1149.1标准里并不是强制要求的,因为通过TMS也可以对TAP状态机进行复位
- STCK:时钟返回信号,在IEEE 1149.1标准里非强制要求,这个信号用于确认指令完成状态
整个状态机的运作过程如下:
系统上电后,TAP状态机进入Test-LogicReset(复位)状态
根据TCK信号上升沿依次进行状态转移
一般情况下会按照以下顺序进行默认检测
- Run-Test/Idle:如果之前没有指令插入,此状态下为空闲
- Select-DR- Scan:扫描设备里所有的数据寄存器
- Select-IR-Scan:扫描设备里所有的指令寄存器
- Capture-IR:某个特定的逻辑序列被加载到指令寄存器中(指令插入预备)
- Shift-IR:将一条特定的指令送到指令寄存器中(强制指令插入)
- Exitl-IR:TAP状态机退出访问指令寄存器状态
- Update-IR:强制更新指令寄存器来执行刚才插入的指令
- Run-Test/Idle:如果之前又指令插入,此状态下会完成对指令寄存器的访问
通过TMS信号对TAP状态进行切换
通过TDl和TDO数据线,将新的数据加载到数据寄存器中;经过一个周期后,就可以捕获数据寄存器中的数据,完成对与数据寄存器的每个寄存器单元相连的芯片引脚的数据更新
正是由于JLINK使用了Cortex-A内核搭配FPGA来实现最快速的TAP状态机,因此才能获得高速性能
JTAG电路原理
JTAG依靠边界扫描(Boundary-Scan)电路完成调试
在芯片的每个IO管脚上都增加一个移位寄存器单元,因为这些移位寄存器单元分布在芯片的边界上,所以被称为边界扫描寄存器(Boundary-Scan Register)。当需要调试芯片时,这些寄存器将芯片与外围电路隔离,实现对芯片输入输出信号的观察和控制
对于输入管脚,可以通过与之相连的边界扫描寄存器单元把数据加载到该管脚中;对于输出管脚,可以通过与之相连的边界扫描寄存器捕获该管脚上的输出信号。正常运行状态下这些BS(边界扫描)寄存器单元对芯片是一套旁路电路,正常运行不受影响
另外,边界扫描寄存器单元可以相互连接起来,在芯片的周围形成一个边界扫描链(Boundary-Scan Chain),它可以实现串行输入输出,当有信号输入的时候,边界扫描链就能获取信号,当CPU要输出信号的时候,边界扫描链也能获取要输出的信号。这样通过相应的时钟信号和控制信号,实现对处在调试状态下的芯片的输入和输出状态的观察和控制。TDI、TDO两个引脚就是负责执行这个工作的
整体数据流向就是TDI->边界扫描链->TDO
CPU跟外界通信的引脚上的数据无非就是指令和数据信号(包括地址跟数据) 两种。但是这两者的结合形成了一个完整的程序,能对它们进行监控就表明我们能进行程序的调试。大多数支持JTAG的芯片都会提供多条独立的边界扫描链,而TAP控制器就是用于控制这些边界扫描链的
上面提到TAP是一个通用的端口,通过TAP,开发者得以访问芯片提供的所有数据寄存器(DR)和指令寄存器(IR)。暂且不提负责数据输入输出的TDI和TDO,先来看看其他几个信号:
- TCK:时钟信号,为TAP的操作提供了一个独立的、基本的时钟信号。
- TMS:模式选择信号,用于控制TAP状态机的转换。
- TRST:复位信号,可以用来对TAP控制器进行复位。这个信号接口在IEEE1149.1标准里并不是强制要求的,因为通过TMS也可以对TAP控制器进行复位。
- STCK:时钟返回信号,在IEEE1149.1标准里非强制要求。
TAP控制器的状态机工作已经在上面的简介部分提过,这里主要强调一下片上硬件实现直接在RTL层面实现状态机即可,但是板级硬件还需要考虑高速传输和与软件兼容的问题,所以调试环节一般需要上位机开发者、板级硬件开发者、片上硬件开发者共同实现。
片上硬件开发者负责TAP控制器的实现,板级硬件开发者负责实现USB到JTAG协议转换,上位机开发者则负责将软件调试接口与JTAG调试协议实现对接。
Keil、IAR、DS-5、ADS开发工具软件等都有一个公共的调试接口RDI,可以通过两种方法实现转换:
- 在电脑上写一个服务程序,把RDI指令解析成能实现的JTAG指令,然后通过一个物理转换接口(这个转换就是板级硬件开发者负责实现的,仅仅起到电气物理层上的转换)发送到目标板。最典型的实现就是
H-JTAG
- 制作一个转接板,直接接收来自上位机软件的RDI指令(一般通过USB总线传输),通过板上的转换芯片将其转换成JTAG指令,再发送给目标板。最典型的实现就是
JLink
第二种方法相对来说速度较快,虽然硬件实现更复杂一些,但是很多公司都有相应的转换芯片,有些特种设备为了可控性和实时性会考虑使用一个高速MCU甚至FPGA作为转换器,相对应用更加广泛
下面要介绍的一个软件和一个硬件分别是调试器上位机OpenOCD和USB-JTAG转接板DAP-Link。使用这两个开源的调试组件配合就可以对大多数ARM设备进行调试了
OpenOCD
OpenOCD是一款开源的调试器软件适配器,旨在提供针对嵌入式设备的调试、系统编程和边界扫描功能。说人话就是OpenOCD是在GDB和JTAG调试器之间的一个桥梁,它需要基于仿真器(JTAG调试器)运行,因为运行OpenOCD的主机往往无法直接解析JTAG信号
有例外,在某些嵌入式Linux设备上,生产商会提供JTAG设备驱动,从而在主机内集成一个JTAG适配器,这类设备往往是脱机烧录器的主控
由于一个仿真器只能支持一个或有限多个传输协议,每个协议涉及不同的电信号,且使用不同的协议栈进行消息传递,底层的设备不能通用,这也就导致了上面的ST-Link和下面的DAP-Link等多种不同的调试器。各大硬件厂商都会推出适用于自己设备的JTAG调试器(比如Xilinx的USB Cable、Espressif的ESP-Prog、ARM的DAP-Link等)。仿真器有时候会被封装成独立的加密狗,这种称为硬件接口加密狗。一些开发板上面直接集成了硬件接口加密狗,这样可以使开发板通过USB直接连到主机上进行调试。常见的芯片包括FTDI的FT2232和一些基于8/16/32位单片机的实现。
目前来看,FTDI出品的FT2232对于OpenOCD的支持比较完善,因此很多厂商都选择这个芯片作为自己的USB转JTAG芯片
OpenOCD可以理解成GDB的底层驱动,GDB必须通过它才能驱动调试器。
OpenOCD可以在官网下载,安装步骤如文件包的INSTALL所述,用automake工具(./configure
)就可以完成安装了
安装完毕后只需要配合各个不同型号外设的脚本文件就可以实现调试了。脚本文件一般由硬件厂商提供,如果是自己搭建的硬件平台则需要按照OpenOCD脚本语法编写。
常用的参数如下:
-h
:等效于--help
用于获取帮助信息--version
:显示openocd版本信息-d
:显示debug等级,可以接从0到3的值-c
:等于--command
,运行后面接的命令-f
:读取后面的配置文件,如果未输入该参数则默认读取openocd.cfg这个文件-s
:等于--search
,后面接要从中寻找配置文件和脚本的目录
OpenOCD使用Jim-Tcl作为自己的脚本解释器,这是一个精简版的Tcl解释器,实现了基本的Tcl命令
部分厂商为OpenOCD提供了python兼容包,这样就能使用Python开发OpenOCD脚本了,不过需要根据情况选择——很多芯片对这种兼容包的支持并不完善
DAPLink
DAPLink是ARM官方推出的一款开源调试器,在之前叫CMSIS-DAP,市面上很多stm32的调试器都基于它设计。DAPLink的软件和硬件都在Github上开源:软件开源;硬件开源
支持以下四个主要功能:
- MSC-拖拽式编程FLASH:将一个DAPLink支持的格式文件保存到DAPLink的虚拟U盘中,DAPLink会自动将文件烧录到连接设备上,完成后DAPLink设备重启并完成烧录。如果发生错误,错误的信息会存放在FAIL.TXT中。支持.bin和.hex文件烧录
- CDC-日志打印、追踪和终端仿真的虚拟串口:DAPLink支持普通的串口IC功能,串行端口直接连接到目标MCU,允许双向通信。它还允许通过在串口上发送中断命令来重置调试设备。支持市面上常见的波特率(从9600到115200)
- HID-CMSIS-DAP兼容式调试接口:可以在任何支持CMSISI-DAP协议的IDE中进行调试,包括openOCD、pyOCD、keil UV、IAR等常见调试软件
- WEBUSB HID-CMSIS-DAP兼容式调试接口:可以使用网页端进行远程调试
目前成熟的DAPLink硬件方案有三个,分别基于以下两种主控
- NXP LPC11U35:官方给出的DAPLink实现,市面上大多数方案都基于该主控
- ST STM32F103CBT6:比较便宜,但是官方还未给出完善的支持,大都是散户自制并开源出来的版本
DAPLink的CMSIS-DAP接口是用于ARM Cortex内核MCU调试仿真的,只要IDE支持CMSIS-DAP协议接口即可使用DAPLink
DAPLink允许通过MSD接口执行简单命令。MSD命令有两种:
对于.act文件,触发DAPLink一个Action(动作)
start_bl.act
该文件将强制进入Bootloader,相当于拔下DAPLink,按住K1再插上。如果DAPlink已经处于Bootloader,则该命令无效start_if.act
该文件将强制DAPLink重新进入DAPLink接口模式。相当于拔下USB并重新将其插入。如果已经处于DAPLink接口模式,则此命令无效assert.act
该文件可以用来测试DAPLink的assert实用程序。将该文件复制到DAPLink MSD驱动器时,DAPLink将生成对util_assert()方法的调用。assert调用导致DAPLink MSD驱动器重新加载一个附加文件ASSERT.TXT,出现在驱动器的根部。这个文件详细说明了断言失败发生的地方(源文件,行号)refresh.act
该文件强制重新加载DAPLink MSD驱动器erase.act
该文件触发对目标FLASH的擦除对于.cfg文件,配置DAPLink一个Configuration(设置)
auto_rst.cfg
该文件用于配置自动复位模式,默认情况下自动复位是禁止的hard_rst.cfg
该文件用于关闭自动复位模式,默认情况下自动复位是禁止的auto_on.cfg
该文件用于打开automation-allowed模式,再该模式下可以出发DAPLink的MSD命令,而不需要按住K1案件。此外,Bootloader更新只允许再该模式下运行auto_off.cfg
该文件用于关闭automation-allowed模式,automation-allowed模式默认是关闭的ovfl_on.cfg
该文件用于打开串口溢出报告。再串口通讯过程中,如果主机PC没有以足够快的速度从DAPLink读取数据,并且发生溢出,则文本<DAPLink: overflow >将出现在串行数据中。串行溢出报告默认关闭ovfl_off.cfg
该文件用于关闭串口溢出报告
下面简要介绍如何移植一个DAPLink
首先要搭建开发环境,安装下面三个软件:
- Python2,版本2.7.9以上,需要将其添加到环境变量,注意不能是python3
- Git,需要将其添加到环境变量
- Keil MDK-ARM,用于测试环境
然后使用git将官方软件包复制到本地
1 | git clone https://github.com/mbedmicro/DAPLink |
在DAPLink目录下安装python的虚拟环境
1 | pip install virtualenv |
然后启动虚拟环境并执行requirements脚本安装python包依赖
1 | venv/Scripts/activate |
最后生成新的MDK工程即可
1 | progen generate -t uvision |
这样就获得了一个新的DAPLink工程,可用于移植
接下来就要针对某个平台使用keil的编译器编译官方源码了
首先打开DAPLink\projectfiles\uvision目录下的工程文件,选择一个自己需要的平台
打开后要安装相应的固件包,安装完成后编译生成.bin文件即可
最后将.bin文件烧录到MCU,进行验证即可
总线工具
传统总线调试方法
树莓派spi与iic调试工具
树莓派上可以使用sudo apt-get install i2c-tools
安装i2c-tools工具包,可以使用树莓派调试IIC总线设备——前提是树莓派要开启iic接口
i2cdetect -y 1
查看挂载在i2c1上的设备
对应后面数字是几就查看树莓派的哪个i2c
i2cdetect -l
检测几组i2c总线在系统上
i2cdump -f -y <iic外设号> <某个寄存器地址>
查看连接iic器件所有寄存器的值
例如
i2cdump -f -y 1 0x20
i2cset -y <iic外设号> <寄存器地址> <寄存器输入值>
设置单个寄存器值
i2cget -y <iic外设号> <寄存器地址>
读取单个寄存器值
嵌入式C语言编码推荐规则
注意:==这里的所有编码规则都只是个人推荐!请一定要选择适合自己的统一易读的编码风格!不要被随意影响!===
一般规则
使用C99标准
虽然C11支持了一些对开发者友好的新特性,但是C11的某些特性在特殊的交叉编译器上可能难以实现,为了保证代码统一性,目前还是应该使用旧一代的C99标准
而且更重要的,可能不是所有人都熟悉C11标准,使用C99这个大部分国内院校教授的C标准有助于工作对接
每个缩进使用4个空格,不要混用tab和空格
主要为了在各个代码编辑器下实现统一规范的代码样式
在关键字和左括号之间使用一个空格,在函数名和左括号之间不使用空格
有助于区分函数和关键字(某些代码编辑器并没有函数/关键字高亮——不针对keil)
1
2
3
4
5/* 示例 */
if (t != 0)
{
int32_t a = sum(1,4);
}尽量不在变量/函数/宏中使用下划线或前缀,除非有意保留统一的编码格式或用于头文件编译
C语言和库的保留参数,懂得都懂
可以适当使用前缀标注特殊的变量性质
例如
interrupt_LED_ON
表示一个用于中断服务函数和主程序通信的全局变量但注意风格一定要统一!
采用统一的分行大括号或同行大括号,不要混杂使用
1
2
3
4
5
6
7
8
9for (i=0;i<N;i++)
{
p++;
}
//文件内采用统一格式
for (i=0;i<N;i++){
}
//不要两种格式混用,否则可能会被打对于一般的变量,使用驼峰命名或下划线命名或其他适合规范的命名法,一定不要使用汉语拼音!一定不要使用汉语拼音!一定不要使用汉语拼音!
例如
HelloWorld
、test_parameter
杜绝
xiaozhanbihu
、cha_bu_duo_de_le
这种诡异的东西在合适的地方采用空格拉大代码间距
1
2
3
4
5
6/* 示例 */
for (i = 0; i < N; i++)
{
a = 3 + 4;
p++;
}这个规范可用可不用,但注意一旦使用了这个格式化方法,一定要保持整个文件都用!否则代码会很丑!
逗号之后使用单空格拉大间距
1
2test(1, 5);
ches(p, q, *r);在调用库函数的时候适合使用这个规则,有助于增加代码可读性和美观程度
不要初始化静态和全局变量为
0
或NULL
编译器会自动完成静态变量、全局变量的初始化工作,不显式写出是为了保证静态变量可以被很明显地看出来
静态变量、全局变量、宏要大写
非常重要的一条规则
这些变量通常都是用于线程/函数/任务之间通信或用于指示系统资源或到处都要用的重要变量,一定要明显标明
有些代码编辑器还会针对大写变量进行高亮
声明要按照以下顺序
- 结构体
- 枚举
- 整数和无符号数
- 浮点数
- 函数
尽量在块/函数的开头声明局部变量
有助于读者快速判断自己需要寻找的变量
尽量在for循环内部声明计数变量
1
2
3
4
5/* 示例 */
for (int i=0;i<N;i++)
{
p--;
}尽量使用
<stdint.h>
中定义的整数类型这是为了保证嵌入式设备中的变量位数确定,这个规范非常重要,通常可以避免一些恶性bug
char、float、double是例外,因为char是4字节,float、double不是整数,所以可以不管
不使用
<stdbool.h>
库,应分别使用1和0表示真假干嵌入式,连1和0都搞不明白就别干了(无慈悲)
如果是接手他人代码,应该保持原有代码风格
除非你自己用自己的代码风格重写一遍
if else
语句应该单独成行并保持大括号1
2
3
4
5
6
7
8
9
10
11
12
13/* 示例 */
if (a)
{
}
else if (b)
{
}
else
{
}switch语句中的break应和case保持相同的缩进
这个规范有助于避免忘记写break
1
2
3
4
5
6
7
8
9
10
11switch (k)
{
case 1:
abc;
break;
case 2:
def;
break;
dafault:
break;
}do-while
语句孩子能够的while应和do部分的右大括号在同一行1
2
3
4do
{
p++;
} while (k);
Linux风格的代码规范
使用doxygen注释格式
其实个人编写的代码并不需要这个,但是如果在大工程或是团队协作中使用这个注释风格将会对工作交接(尤其是后期编写文档和展示PPT)很有帮助
所有注释使用
/* */
,即使是单行注释对于需要特殊注明的语句使用
//
不一定要扔掉
goto
语句goto
在单函数内进行条件返回的情况很好用但是要注意:对应的标记不论周围代码缩进如何,总保持不缩进状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/* 示例 */
void Test(void)
{
xxx;
xxx;
if (i)
{
xxx;
FLAG: //不要缩进
yyy;
}
yyy;
xxx;
if (j)
{
goto FLAG; //这里使用了goto
}
return;
}goto代码旁边最好也通过注释显式强调
使用<>尖括号来引用C标准库的头文件,使用””双引号来引用自定义头文件
使用以下指针风格
1
2
3
4
5/* 示例 */
int* p; //C++风格指针,用于声明很简单的指针
float *check_pointer; //C风格指针,用于一般指针的声明
uint8_t *t = (uint8_t*)check_pointer; //指针强制类型转换使用以下风格的注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/* 示例 */
//多行注释
/*
* comment
* comment
* comment
*/
/**
* comment
* comment
* comment
* comment
*/
//单行注释
/*
* comment
*/普通函数固定采用下划线命名法,对结构体进行操作的函数固定采用驼峰命名法,结构体变量应固定采用驼峰命名法
这是为了和C++的对象和方法统一风格
函数返回值独立成行
所有局部指针变量声明在同一行,用逗号隔开
这个是Linux编码的一个风格,可用可不用
结构体使用typedef再命名后可以使用
_t
后缀,但是原有的结构体名称不能使用后缀应该使用C99的结构体初始化风格,避免使用C11风格的初始化器
主要用于保持代码风格统一和简洁易读
函数句柄应该使用
_t
后缀大括号一定要保持良好缩进
空while循环、do-while循环都要包含花括号,空for循环要用分号结束
1
2
3while (1) {}
do {} while (1)
for (;;) ;
个人常用有助于避免bug的编码习惯
使用指针后加入对指针非空的检查
1
2
3
4
5
6
7
8/* 示例 */
int* p;
p=(int*)malloc(sizeof(int));
if (p == NULL)
{
return -1;
}调用有提供返回值检查的库函数后对返回值进行检查,如果出错就返回报错信息
分清前增量和后增量的差别
使用
size_t
作为长度或大小指示变量在自己写的库里多使用
const
保证不会被nt误用函数多使用括号和
sizeof
来避免计算顺序和变量长度问题头文件的变量一定要用extern声明并在对应的.c文件中定义
头文件中最好使用C++检查来警惕有人把C库放进C++里调用
如果没有对算法部分的优化或特殊考量,千万不要使用递归函数!
定义宏函数时可以使用以下保护方法
- 加入圆括号保护变量和结果
- 使用do…while(0)保护多语句
- 不要使用分号结尾
对宏语句和预处理语句不要进行缩进!
文件末尾留下一个空行
用于应对某些讨厌的编译器/IDE报错
在头文件中包含许可证
在个人项目中能保护自己的版权,在大型项目中能够保护自己和同伙的版权
使用
#program once
或#ifndef
保护头文件头文件应该只声明公共变量/结构体/枚举/函数
避免重复包含
千万不要包含.c文件!
Doxygen
这是一个为所有函数、结构或其他代码段落添加描述的辅助系统
使用Doxygen可以直接通过代码中的注释输出API文档。基本语法如下所示:
由两个星开始的代码注释会被Doxygen解析,但带一个星号的注释会被忽略
1
2/** 会被解析的注释内容 */
/* 不会被解析的注释内容 */注释应放在函数和结构体前面
文件头部需要使用以下格式
1
/** \file */
否则文件不会被Doxygen解析
使用
\param
来输入参数使用
\return
来列出返回值使用
\ref
来指向其他文档元素(包括函数和文件)的交叉参考(这其实是对LaTex的模仿)
可以使用
@
来替代\
完成上述设置(对JavaDoc的模仿)
使用指令doxygen -g
来获得一个配置文件
STM32的标准库、HAL库和LL库,RT-Thread的源码,lwIP的源码等都遵循了Doxygen原则,很多基于Eclipse的IDE也都内置了Doxygen格式化器。一个典型的Doxygen文档如下所示(摘自STM32F407标准库)
1 | /** |
Doxygen生成的文档如下所示(摘自RT-Thread)
使用命令行调用keil的API
keil提供了Windows下CMD的API,只要把keil安装目录加入环境变量Path或使用绝对路径即可
基础指令
1 | <UV4> <command> <project_file> <options> |
编译示例
1 | d:\Keil\MDK\UV4.exe -r Template.uvproj -o Build_output.bin |
这里编译出的文件保存为Build_output.bin
烧录示例
1 | d:\Keil\MDK\UV4.exe -f Template.uvproj -o Build_output.bin |
这里将以MDK里选定/默认的烧录方式烧录bin文件
使用VSCode直接调用keil指令
VSCode中下载插件Cortex-Debug
和keil assistant
即可直接使用插件控制keil工程
如上图填写keil的exe文件目录即可使用