目录
第一阶段:C 基础
<一>:数据类型
1:基础架构及变量输出
2:变量定义及初始化
3:变量输入scanf
4:常量const
5:浮点数float/double
①:认识浮点数
②:浮点数的运算
③:格式化输出
6:字符变量char
<二>:运算符
1. 算术运算符
2. 关系运算符
3. 逻辑运算符
4. 赋值运算符
5. 条件运算符(三元)
6. 自增自减
7. 运算符优先级
<三>:输入输出
①:printf — 格式化输出
②:scanf — 格式化输入
③:getchar / putchar — 单字符输入输出
<四>:控制流结构
①:if-else
②:else if
③:switch-case
④:for 循环
⑤:while 循环
⑥:do-while循环
⑦:break 和 continue
⑧:goto
第一阶段:C 基础
<一>:数据类型
1:基础架构及变量输出
#include <stdio.h> int main() { printf("Hello, World!\n"); }2:变量定义及初始化
#include <stdio.h> int main(void) { int a = 10; int b = 20; int c = a + b; printf("a = %d\n", a); printf("b = %d\n", b); printf("c = %d\n", c); return 0; }3:变量输入scanf
#include <stdio.h> int main(void) { int a; int b; int c; printf("请输入两个整数:"); scanf("%d %d", &a, &b); c = a + b; printf("a = %d\n", a); printf("b = %d\n", b); printf("c = %d\n", c); return 0; }scanf()的格式字符串决定了它如何解析输入,你写什么格式,它就按照什么规则去匹配。
scanf("%d %d", &a, &b); 这里的占位符%d %d,中间没有任何符号隔开,
很多初学者误认为:必须输入一个空格,其实不是,它代表跳过所有空白字符,
因此空格,Tab,或者换行输入数值,均可运行。
scanf("%d ,%d", &a, &b); 这里的占位符%d %d,中间用了一个逗号隔开,
因此输入数值时,需要输入,比如10,随后用逗号隔开,再输入另外一个数值,比如20
即 10,20
4:常量const
const int a; 和int const a; 是两种不同的写法,第二种写法少见,但第一个等价
在指针后面会遇到,不过初学基础用法,学习const int a即可。
5:浮点数float/double
①:认识浮点数
②:浮点数的运算
③:格式化输出
举个例子:
printf("%5.4f\n", a);
数值3.1415,要求保留 4位小数,3.1415的字符:3.1415,一共 6 个字符
printf("%5.4f\n", a);你要求的总宽度是5。但是内容本身已经占了6 个位置(比5大)。
规则触发:当要求的宽度 < 实际内容的宽度时,宽度限制失效,内容完整输出。
结果:3.1415(前面没有空格)。
printf("%8.4f\n", a);你要求的总宽度是8。但是内容本身已经占了6 个位置(比8小)。
结果:␣␣3.1415(前面有两个空格)。
6:字符变量char
<二>:运算符
1. 算术运算符
+ - * / %/要注意的东西之前说过了:整数/整数 = 整数(截断)。%只能用于整数。
嵌入式里几乎全是整数运算,浮点运算要么避免、要么用硬件 FPU。
2. 关系运算符
> < >= <= == !===(等于)和=(赋值)是最容易写错的:
if (x == 5) // 判断 if (x = 5) // 赋值!= 5,非 0 就是 true,条件永远成立一个防御写法:常量放左边if (5 == x),写错成if (5 = x)编译器直接报错。不过现在编译器对if (x = 5)一般也会警告。
3. 逻辑运算符
&& // 与:两边都为真才为真 || // 或:一边为真就为真 ! // 非:取反短路求值:&&左边为假,右边就不算了;||左边为真,右边就不算了。
if (ptr != NULL && ptr->value == 10) // ptr 为空时不会去访问 ptr->value嵌入式里也常用!翻转一个标志位:
flag = !flag; // 0 变 1,1 变 04. 赋值运算符
= += -= *= /= %=这几个我比较会
&= |= ^= <<= >>=这几个不知道啥意思
&=:按位与赋值,a = a & b,清零某些位 / 保留特定位
|=:按位或赋值,a = a | b,清零某些位 / 保留特定位
^=是按位异或,a ^= b,等价于a = a ^ b,翻转寄存器的某一位
<<=: 左移赋值,a = a << b,a <<= 2;// a 左移2位
>>=:右移赋值,a = a >> b,a >>= 1;// a 右移1位
~:按位取非
复合赋值就一个意思:x += 5等价于x = x + 5。嵌入式里位运算的复合赋值极为常用:
REG |= (1 << 3); // 置位 REG &= ~(1 << 3); // 清零举例:a=0000 0001左移3位0000 1000得0x08,第3位(bit3)为1,其余位为0
~是按位取反,~a表示每一位反过来得11110111,除了bit3是0,其他位都是1
5. 条件运算符(三元)
max = (a > b) ? a : b; // 条件 ? 真值 : 假值if (state) { PORTA |= (1 << 3); // 置位 } else { PORTA &= ~(1 << 3); // 清位 }PORTA = state ? (PORTA | (1 << 3)) : (PORTA & ~(1 << 3));嵌套可读性会崩,不建议套娃。
6. 自增自减
int a = 5; int b = a++; // b = 5, a = 6 (先用后加) int c = ++a; // c = 7, a = 7 (先加后用)单独写一条语句时(i++;)没区别。但和别的运算混在一起时要注意,嵌入式里常见的坑:
7. 运算符优先级
不需要全背,记住三条:
| 优先级 | 运算符 |
|---|---|
| 最高 | ()[]->. |
| 中间 | */%>+-><<>>> 比较 > 逻辑 |
| 最低 | = |
不确定就加括号。嵌入式代码可靠性第一,多写一对括号不丢人:
if ((a & MASK) == VALUE) // & 优先级比 == 低,不加括号是 if (a & (MASK == VALUE))<三>:输入输出
①:printf— 格式化输出
| 格式符 | 含义 | 嵌入式场景 |
|---|---|---|
%d | 有符号十进制 int | 普通数值 |
%u | 无符号十进制 | 计数器、时间戳(unsigned 是嵌入式常态) |
%x/%X | 十六进制(小写/大写) | 查看寄存器值、地址,嵌入式最常用 |
%c | 单个字符 | 命令解析 |
%s | 字符串 | 日志输出 |
%f | float / double | 传感器数值 |
%p | 指针地址 | 调试时查看指针值 |
%% | 打印一个% |
修饰符回顾(上次讲的):
printf("%08x\n", reg); // 08 = 总宽 8,不够前面补 0 → 0000a3f1 printf("0x%02X\n", val); // 0xA3 这种格式,嵌入式打印寄存器非常常用②:scanf— 格式化输入
int n; scanf("%d", &n); // 一定要加 &(取地址) scanf("%x", &n); // 按十六进制读 char c; scanf(" %c", &c); // %c 前面空格很重要!跳过上次留下的 \n嵌入式里scanf用得不多,更常见的是从串口中断读字符自己拼。但学 C 阶段知道就行。
③:getchar/putchar— 单字符输入输出
char c = getchar(); // 读一个字符 putchar(c); // 写一个字符嵌入式里这俩通常被重定向到串口:
// 重定向 putchar 后,printf 就可以走串口输出了 int fputc(int ch, FILE *f) { while (!(USART1->SR & USART_SR_TXE)); // 等发送缓冲区空 USART1->DR = ch; return ch; }<四>:控制流结构
①:if-else
if (条件) { ... } else if (条件) { ... } else { ... }嵌入式应用:轮询标志位、判断状态。
if (USART1->SR & USART_SR_RXNE) { // 接收非空 = 来数据了 data = USART1->DR; }②:else if
你想要的逻辑是"互斥"的——满足一个就不再往下看。这时候就需要else if:
只是不断缩进太难看了,所以把else { if ... }的括号和缩进去掉,写成else if,C 语言里所有人都这么写。
执行逻辑:从上到下,找到一个满足的就执行,然后跳过后面所有的else if和else。
③:switch-case
多个条件比较同一个值时,比写一堆if-else更清晰:
switch (cmd) { case 0x01: led_on(); break; // 忘记 break 会"穿透"到下一个 case case 0x02: led_off(); break; default: // 没匹配到时走这里 send_error(); break; }嵌入式里的典型用法:命令解析、状态机。
switch (state) { case STATE_IDLE: if (start_condition) state = STATE_RUNNING; break; case STATE_RUNNING: do_work(); if (done) state = STATE_DONE; break; case STATE_DONE: cleanup(); state = STATE_IDLE; break; }注意:case后面的值必须是整数常量。字符串不能直接放case。
④:for循环
for (初始化; 条件; 每次循环后执行) { ... }for (int i = 0; i < 10; i++) { printf("%d\n", i); }三个部分都可以省略(极端简化):
for (;;) { // 等价于 while(1),嵌入式主循环标配 ... }实际上大多数嵌入式main函数长这样:
int main(void) { hardware_init(); while (1) { // 或 for(;;) read_sensors(); process_data(); update_outputs(); } }for循环执行次数详解
⑤:while循环
while (条件) { ... } // 条件为假时一次都不执行嵌入式里等待硬件就绪:
while (!(USART1->SR & USART_SR_TC)) { ; // 空循环,等待发送完成。注意要加 volatile! }⑥:do-while循环
do { ... } while (条件); // 无论如何至少跑一次用得比while少,多用在宏定义里(保证宏像一条语句一样安全):
#define LED_TOGGLE() do { LED_ON(); delay(100); LED_OFF(); } while(0)⑦:break和continue
break; // 直接跳出当前循环 continue; // 跳过本次循环剩下的部分,开始下一次break也用于switch里跳出(不要混淆——break只跳出最内层的一层循环或 switch)。
⑧:goto
唯一合理的用法:统一出错处理,避免到处写重复的清理代码。
void process(void) { if (init_step1() != OK) goto error; if (init_step2() != OK) goto error; if (init_step3() != OK) goto error; do_work(); return; error: cleanup_all(); set_error_flag(); }规则:只往前跳、跳到一个统一的出口标签。不要往回跳,不要乱飞——那样确实会写出意大利面条代码。