一、GPIO介绍
# v* |% I* Y* F) S- [7 E) z 1.1 GPIO 简述4 e* J% W( p5 x
GPIO(General purpose input/output,通用型输入输出),一个引脚可以用于输入、输出或其他特殊功能,PIN脚依现实需要可作为通用输入(GPI)或通用输出(GPO)或通用输入与输出(GPIO),并多个GPIO一起组合可实现诸如UART、SPI等外设功能。; b) e8 \* |7 }& D7 }
6 C3 V6 n d! l. u& u2 ], P
GPIO是通过寄存器操作来实现电平信号输入输出的,对于输入,通过读取输入数据寄存器(IDR,Input Data Register)来确定引脚电位的高低;对于输出,通过写入输出数据寄存器(ODR,Output Data Register)来让这个引脚输出高电位或者低电位;对于其他特殊功能,则有另外的寄存器来控制它们。GPIO的输出通常可以输出0/1信号,用来实现如LED灯、继电器、蜂鸣器等控制,而GPIO的输入可识别0/1信号,用来实现开关、按键等等动作或状态判定。
2 o2 B3 W/ Z3 ~5 O$ W" L3 K4 \3 R& e0 E$ B
& ^; q: O; ?: g4 E& `% W
. }" x0 k4 ?9 A3 T
' z# n( o0 h( B* P2 ?& M3 L; p
1.2 STM32 GPIO寄存器0 j- _5 {6 E g2 [! ~+ q! V
STM32中GPIO的寄存器是MCU分配的一个连续存储区域,按存储地址划分出不同功能寄存器,如下图所示,有Port mode寄存器、Output Type寄存器等等。& I, \! x& p+ v4 Q6 R0 r2 @8 m4 r
+ ?9 Z: |$ J* ~% g7 C' R( L7 q2 k& M3 {
5 E# U9 a+ W4 f* s( f3 h- d
! s1 y% G. M: _3 A. ^% [* @1 \
在HLA库中,这些寄存器的数值(宏定义)通常会放在/Drivers/CMSIS/Device/ST/STM32L4xx/Include/stm32XXX.h头文件中,供开发者调用,例如STM32L496VGTx的开发板,该头文件具体名是stm32l496xx.h,自8977行起到10008行,具是按上述表宏定义了GPIO各寄存器的各个数据位可设置数据值(偏移(POS)及相应数值)。% \' }; F/ i+ D Y. l5 v Y
3 d. [5 L5 J0 H6 r1 V; s L' ]' g
: M% l9 u! _4 y+ M. `' \( S: a
F R D0 \+ g! |$ n9 N" d 1.3 GPIO引脚与标记
% ~: ]) ?+ z R9 G5 q" k/ g 在一个STM32 MCU芯片中,所有引脚本质都是GPIO引脚,只是鉴于全GPIO引脚开发者使用起来比较不方便,于是才通过各个GPIO组合形成针对各个外设的IO接口。在CubeMX中,以STM32L496VGTx芯片为例,可以直观看到该芯片有100个引脚:LQFP100,LQFP是封装,100指的是引脚数。* N0 j& U$ V+ y# g6 m
9 p7 F% z3 A- V6 H! g* Q: }
$ V- j. A9 _; j, C# G4 G
- I2 R" }: s( b+ T# I8 R# J STM32的MCU标记一般有两种,直插IC以半圆条口为标记,贴片IC以小圆点为标记,也有缺口及小点都有标记的。通常将标记圆点或缺口正面朝左上角,左上角的下方脚为第一脚,依次围绕芯片中心逆时针计数,在旋转360度以后,左上角右方角为最后一脚。如上图所示,STM32L496VGTx引脚序号如蓝色字体及箭头所示标记。STM32的MCU的引脚标记有三种方式:
2 D* A" j7 s l4 y3 x+ k$ i3 h# e) V3 k7 p/ X& @. q
一种是基于上面所述的100个序号排序P1、Pn等,这种标记目前一般是在直接操作寄存器地址或使用标准库中使用,目前HLA库、LL库已经不提供直接地址偏移方式的API了,而是给没一组分组提供了独立寄存器来处理该组引脚;
. L. V+ l4 Q3 J% Y* X7 j. U3 k4 h- [ z
第二种是标准库或HLA库宏定义了引脚分组,一般为A、B、C、...,按大写字母次序网线,目前支持到16组类别,每组为PX0~PX15,其标记就是分组名+0~15序号,例如GPIOB+GPIO_PIN_3。) p/ q1 m* G- I3 \7 n; D0 i+ y
8 x, F/ _. [$ L! u4 y 注意,目前STM32的MCU设计是最大16组类别,支持到256引脚(16*16),通过STM32命名规范可以得知,STM32最做引脚支持到256个,但一般会少于引脚数/16的组数,因为不少引脚在芯片设计时就特别支持了其引脚标识,如VSS、VDD等。另外也不是每组必须0~15序号全用完,分组主要是方便设计及使用时进行引脚区分而已。+ K, c( e* p# y4 s
/ q: `5 L3 B2 ?/ \& t# Y" J' d
! e* o3 i4 h, j4 ^8 @/ `+ u4 U6 S; Q* N! l2 ^: e# D
目前HLA库主要就是这种方式,例如来看看GPIO读取数据的API(HAL_GPIO_ReadPin)实现,主要就是根据引脚序号(0~15)来读取IDR寄存器内对应数据位的数值。
, ^0 i4 m Y. i+ Y
; C H! C' V- }! K% Y1 N7 e
9 Z$ }! B: J, w3 z/ ~7 w8 b' b0 E8 N" W+ q8 s: P. f$ ?' h% d
GPIO分组引脚地址在HLA宏定义数值如下。
$ I+ p* B" `1 p4 ?. {4 U- #define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */
, J" T! ?( b5 x9 n, A - #define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */: e6 k( \7 E( S: m& h
- #define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected *// T/ X4 k& M6 p. Y% M9 ?
- #define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */
- x" P# s) g$ e( \8 x+ { - #define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */
5 J! V: c0 |8 V* t C) s5 | - #define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */
: j! g( L W8 g( t% M - #define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */
) w c8 X5 g2 [& k5 a - #define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */
' N; M4 _3 n+ `6 l1 N - #define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */
+ M) L& x3 M( f: q( Y3 u - #define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */
- I a. A, M4 X - #define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */9 h; r0 G3 O4 v( w
- #define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */# N' p% D. w4 N" o) f2 }6 e
- #define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
8 K i3 D0 P/ W1 w e0 R - #define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */
5 t+ X. d6 S* H7 J# @) a9 V5 d - #define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */; O5 T4 e4 l- f8 d4 E) u8 e
- #define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */
2 l6 n' F( M8 T# k1 Z) `; ] - #define GPIO_PIN_All ((uint16_t)0xFFFF) /* All pins selected */
复制代码
" }9 b3 P$ f4 p Q 而GPIO各个分组的IDR寄存器为32bit宽度的数据,目前只用到低16位字段。
- ]% s y" g: F" _0 [2 \# X; q0 ^- typedef struct" O) s3 A: ]( o0 w
- {( i5 v) N" V5 T7 `* e9 L: {" q! k
- __IO uint32_t MODER; /*!< GPIO port mode register, Address offset: 0x00 */
, q2 y5 w$ {! s& L! ] - __IO uint32_t OTYPER; /*!< GPIO port output type register, Address offset: 0x04 */, ~2 [2 H+ {, o$ r! o5 Q
- __IO uint32_t OSPEEDR; /*!< GPIO port output speed register, Address offset: 0x08 */
# r: c8 D( ^4 R. m; j5 e+ g4 B - __IO uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register, Address offset: 0x0C */7 J/ K! t$ ]! d* J
- __IO uint32_t IDR; /*!< GPIO port input data register, Address offset: 0x10 */
0 C. S# }7 S" `; t3 { - __IO uint32_t ODR; /*!< GPIO port output data register, Address offset: 0x14 */
) c' |9 b; r5 a3 ~ @( J - __IO uint32_t BSRR; /*!< GPIO port bit set/reset register, Address offset: 0x18 */* _$ Q6 I5 k! M/ I9 ]: ~
- __IO uint32_t LCKR; /*!< GPIO port configuration lock register, Address offset: 0x1C */5 Q& y9 p: q( `: N2 r1 I
- __IO uint32_t AFR[2]; /*!< GPIO alternate function registers, Address offset: 0x20-0x24 */
; R, e6 P$ w6 O/ n Z! n+ d - __IO uint32_t BRR; /*!< GPIO Bit Reset register, Address offset: 0x28 */, v5 f! o2 I* m# _, h
-
/ O6 @# I* V' [/ _+ ]; k! d - } GPIO_TypeDef;
复制代码 , g' s3 T! f$ m) [, }2 q3 e
第三种引脚标记其实就是在第二种方式基础上,为引脚设置别名,在cubeMX设置引脚mode后,会开启该引脚,在引脚参数设置列表可以设置该引脚标识名,例如下图,设置PE3(P2)为输出模式(GPIO_Output),的别名为LED1。* V5 L7 _- ?$ a; p' A! s/ l0 G
( B, k, ~6 Q! p. F* M; X3 f
1 `/ ^3 z9 \" @+ H
) W6 F J) `1 N8 X% J
那么在生成输出代码时,会通过#define实现标识名替换。% w# n6 I6 |1 x/ A
4 |- `' A5 Y/ T5 G+ V* g
, e: a9 M2 a( I1 L0 n; U2 g
+ X" y$ V1 {: u6 M. |2 L. w3 @/ p. \' \9 b9 z/ N7 r7 Y/ `
1.4 GPIO原理框图及CubeMX配置
& a: C5 Z3 y6 ^) Y3 { 进一步,再看看GPIO结构原理图,它支持4种GPIO输入模式(浮空输入、上拉输入、下拉输入、模拟输入)和4种GPIO输出模式(开漏输出、开漏复用输出、推挽输出、推挽复用输出)。因此才能通过多种输入输出模式组合来实现更复杂的其他外设功能。
/ P8 g" S5 y W" X. L( ]) Q
3 k0 [% k0 C& z! r+ M) Y* w2 o
; ^' j8 @# c( d) \
) L6 G6 ?+ c+ J1 a+ g 在cubeMX中在GPIO配置界面中,可以配置GPIO引脚的GPIO输入模式以及其他参数设置。
$ K a" h9 g8 m. q/ G' N
$ `9 I8 c( N0 K# f! P3 J& @
6 Z$ R% G! Z9 M6 _' T& t/ U
1 F) i5 x) j3 X, c0 o5 k 同样地,在cubeMX中在GPIO配置界面中,可以配置GPIO引脚的GPIO输出模式以及其他参数设置。
* E! Z% @6 [$ F5 v* D/ @4 \% S: s( r5 }
8 _% J m6 S, h7 }6 x
% u0 Q' z. }/ {1 W; p' N
另外各种基于GPIO组合实现的外设,也可在GPIO栏的各个外设页面下,单独设置各个引脚的GPIO输入或输出模式。
% @/ \* V$ E# m8 {+ |' R7 ]2 u: f1 K" S
/ m/ b, h& U5 F7 r- J, w1 K! C
" v* Z5 \/ f) a3 f$ }9 E 二、GPIO的HLA源码分析
* z4 q' r' [, ^9 `7 z 2.1 HLA库GPIO初始化4 C6 H& X L' W8 u- s/ S' M: _
通过cubeMX生成输出源码,如果配置GPIO引脚功能,则cubeMX生成代码时,设置了为每个外设生成独立.h/.c文件时,会在Core源码目录下的Inc及Src目录,分别生成gpio.h和gpio.c驱动文件,否则将会GPOI相关输出函数放置main.h和main.c中。
0 l5 @/ ~0 ?8 `2 P- w+ S' Z- S: O! y
在gpio.c文件中,主要定义了MX_GPIO_Init函数,该主要做三件事情,一是启动各个GPIO分组寄存器的时钟,二是对于输出GPIO引脚实现初始值写入,三是将在cubeMX上配置引脚参数写入到参数缓存区域,并依据缓存参数,调用HLA库的HAL_GPIO_Init来实现真正的初始化设定。如下面代码所示。) x1 G: B7 K- Y( X: D: q( d* F
" [9 q/ N/ J+ Q& z
- //main.h6 J# |" t1 g9 ~: C2 R( O+ {& Y. C
- /* Private defines -----------------------------------------------------------*/
$ J: K! e! p1 m. w - #define LED1_Pin GPIO_PIN_34 y" x! t, \+ \* l7 T- v: D
- #define LED1_GPIO_Port GPIOE) @/ f: B" P$ z
- #define KEY2_Pin GPIO_PIN_103 B. p+ T3 W8 |
- #define KEY2_GPIO_Port GPIOE2 [/ m& y! I; q( s' p
- #define KEY0_Pin GPIO_PIN_11
) ?9 v0 k* z; J - #define KEY0_GPIO_Port GPIOE! b4 U" l! G6 L' t
- #define KEY1_Pin GPIO_PIN_149 C& t* R( A6 k6 D7 q: s, `2 K
- #define KEY1_GPIO_Port GPIOE
7 b5 A) o1 u) C8 W' T6 w: G - #define LED2_Pin GPIO_PIN_158 q( }/ [ R: L5 h' p6 ?
- #define LED2_GPIO_Port GPIOD3 Q; g4 T/ _7 ?6 a) q$ |
- #define LED0_Pin GPIO_PIN_6, g5 N" S# J' ^
- #define LED0_GPIO_Port GPIOB
* S/ b" ~% O1 m+ q8 |* x - /* USER CODE BEGIN Private defines */
7 s8 o0 ^. }# S9 ^1 t - //gpio.c! d! ? E) q# ]
- void MX_GPIO_Init(void)5 e% v3 p! G( z1 |$ j. I$ m
- {. Y! i4 k6 P$ a) X$ e8 N. ?
-
. i: \+ I$ {. u# [* G U: Y - GPIO_InitTypeDef GPIO_InitStruct = {0};8 s5 x9 G" R8 {% ~! I; ^/ c3 r L6 X
-
+ @% r- w( |/ D Y. n - /* GPIO Ports Clock Enable */
/ z. g/ m; A5 Y5 C8 V. w3 g$ F - __HAL_RCC_GPIOE_CLK_ENABLE();9 p2 P0 R! u2 m8 j7 I7 _8 h
- __HAL_RCC_GPIOC_CLK_ENABLE();$ h" q, v5 k! j: \- }* ^! X
- __HAL_RCC_GPIOH_CLK_ENABLE();
9 j8 K) ^" t$ ^# D e3 B" ]) l - __HAL_RCC_GPIOB_CLK_ENABLE();6 O5 K2 C2 P8 v
- __HAL_RCC_GPIOD_CLK_ENABLE();3 t8 n( C! n* f5 K
- __HAL_RCC_GPIOA_CLK_ENABLE();
/ h: J/ f7 M3 x! P$ E - 3 y" r9 l& ]. X6 G
- /*Configure GPIO pin Output Level */1 _/ |" j8 S% O" r! U( o3 p" K3 i
- HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
$ g! d1 u0 k/ x5 b3 o! E1 O8 [! D -
! O5 c, ^ U9 _ - /*Configure GPIO pin Output Level */) c2 h8 ` T! ]/ T$ h7 _
- HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);9 P6 z* w5 {: a! O0 a0 J5 t, Q
-
! p# x+ F! ?5 d9 q3 B- `- u. G+ w - /*Configure GPIO pin Output Level */2 E6 R M) M; p z, Y
- HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
. n* m% B7 Z% }+ y) [& R -
$ I) d, g- f/ e - /*Configure GPIO pin : PtPin */
- X4 \- K& }0 Z% @; p - GPIO_InitStruct.Pin = LED1_Pin;
6 e# P9 g9 I8 S# R3 y - GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; a$ ^8 l3 Z) k
- GPIO_InitStruct.Pull = GPIO_NOPULL;, ^6 j* s% T3 y
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;: z. J* ~5 L/ K1 M ~$ t+ K/ g# F2 Z
- HAL_GPIO_Init(LED1_GPIO_Port, &GPIO_InitStruct);& Z, L7 e { ~6 V6 ]6 A! c
-
8 |. P/ p9 g! M* g' l: ^% @) [4 R - /*Configure GPIO pins : PEPin PEPin PEPin */
: G. ~# K$ \, ^) r* _; r# ?: t - GPIO_InitStruct.Pin = KEY2_Pin|KEY0_Pin|KEY1_Pin;
* x3 E- e0 k7 K/ @6 r2 y - GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
% z& v; Z" F1 p6 ] - GPIO_InitStruct.Pull = GPIO_NOPULL;
" }+ I [& x- m2 _" ]9 l - HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);( w3 w3 b7 w; e
- , i+ c8 U8 {2 K& ^, p* {
- /*Configure GPIO pin : PtPin */
$ Z: `( G6 ^, a8 u0 | - GPIO_InitStruct.Pin = LED2_Pin;
3 @% a' h$ K( O9 T$ T, } - GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
9 p( l9 o: r9 D! `! E - GPIO_InitStruct.Pull = GPIO_NOPULL;
$ I/ y$ ^2 {1 ?8 J* t2 M% `* n/ r - GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;1 S3 [0 I; ?1 v) T; a t
- HAL_GPIO_Init(LED2_GPIO_Port, &GPIO_InitStruct);# |( O! n* d1 g2 K6 G$ b- E7 j
-
7 T0 O: z3 T7 z: Q( {# { - /*Configure GPIO pin : PtPin */
6 n; \# S: b" ?( @& i1 \ y/ _ - GPIO_InitStruct.Pin = LED0_Pin;
6 f) k N# S' s( w, A6 C$ x - GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;: G4 K: b% c4 y1 h% {" h
- GPIO_InitStruct.Pull = GPIO_NOPULL;
. t3 ~6 P( L4 d2 k& W& M - GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
; }. L- c/ q$ v2 r - HAL_GPIO_Init(LED0_GPIO_Port, &GPIO_InitStruct);, @; B! Q/ i( h, z9 h, R0 y0 b# y
- & t2 e: P, F1 B# }8 F# Q6 n% y
- }
复制代码
. n+ T" E3 s0 E- Y" [( _# S3 M+ Z 在stm32l4xx_hal_gpio.c源文件中定义了HAL_GPIO_Init函数,它做以下事情:1)诊断先前缓存配置参数是否合规,2)再将缓存配置参数写入到GPIO的寄存器内,完成GPIO的初始化设定(GPIO 的Mode、Pull、Analog、Alternate、Direction、EXTI等)。6 M# r- h6 _) a( G) u* e
& U& H8 @% |: V$ D* u9 f7 R
' O" r% x/ h$ o& |9 Q
7 U' V/ A4 G3 E6 z- H: X6 Q
2.2 GPIO的开发应用接口API简述$ N$ ]/ I: j% Z/ c# o6 D9 M
除了提供GPIO初始化API外,STM32的HLA针对GPIO还外开发这提供如下API:7 r8 N5 A# D0 j# c1 V; s; ]
, U0 n \8 o! x/ ^( \& ~0 q
9 d: b- k+ }6 R ?9 B/ f
, y9 n0 I" g' e) X: ~ GPIO对数据的读取主要是HAL_GPIO_ReadPin函数,写入主要是HAL_GPIO_WritePin、HAL_GPIO_TogglePin函数,其参数都是GPIO_TypeDef* GPIOx和 uint16_t GPIO_Pin,前者就是前面参数的分组如GPIOA、GPIOB等,后者就是Pin0~Pin15,例如GPIO_PIN_3。
: l5 x2 e7 ?/ x2 K; B, C3 p) V- O( b; B% B$ @# u* `% ?# {
关于外部中断回调函数HAL_GPIO_EXTI_Callback,该函数是弱函数
9 t" n1 U# }$ A" R% e) h6 y/ M. d# R
- Y" s- j, }. b) p
( F, m2 _ u5 E0 y, C 若需要使用该函数,使其生效,首先需要在cubeMX配置界面,将GPIO引脚调整为GPIO_EXTI类型
{( U9 E2 P3 y' Z. C0 _5 y2 d, T- ~" j( J7 X' Q2 F- @
5 `9 J; I9 I1 m! Z8 V+ G: [
" \0 j6 k5 r! ~% _
并开启该引脚的中断支持
% R5 Z) J/ s, S& ^. P* o* e+ _0 H: H( f
- R9 M/ C0 `+ B* r2 `0 \- k* l2 a; ^3 l/ m& v1 R* y. M% y
就可以重新实现stm32l4xx_hal_gpio.c文件中的HAL_GPIO_EXTI_Callback函数来实现GPIO外部中断回调功能,例如设置如下,则实现按键KEY0按下异常,触发回调函数HAL_GPIO_EXTI_Callback调用,从而实现LED0灯状态反转(点亮或熄灭):
% e) ~: j4 m3 u9 d5 L8 N- /* USER CODE BEGIN 0 */8 \9 I$ }/ N4 }' g7 p3 R
- void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
: }7 a K/ E3 n" u) a% P: z - {) t$ G l. v: h* v! c5 f- _% o
- if(GPIO_Pin==KEY0_Pin)
0 G0 `6 |7 x" G7 t$ {! \9 \ - {
- E8 U, a d. Y% }' | - static uint32_t key0_count = 0;' b( K% ~; D: u6 |: R
- printf("this is %lu count for HAL_GPIO_EXTI_Callback!\r\n",key0_count++);
. u' n% W3 O: v1 F - Toggle_led0();//LED0等状态反转一次3 Q8 o' f% @) v- ^. w
- }
6 B# S) m% \. i; V. U% g' K3 K! M - }
5 W3 o6 c$ Q" D0 \9 O m2 F - /* USER CODE END 0 */
复制代码
; c, b8 l3 a; b! @ 同样地对于外部中断请求函数HAL_GPIO_EXTI_IRQHandler也可以类似地进行调整实现GPIO外部中断触发器调用,从而实现我们需要的业务应用。
, U- f; r0 M4 K7 y. [, E3 w9 c: I2 @! j; n! M2 I
GPIO的HLA库中,HAL_GPIO_LockPin函数,主要是操作lock寄存器数值的,lock寄存器会针对GPIOx_MODER, GPIOx_OTYPER, GPIOx_OSPEEDR等GPIO寄存器配置对应的状态标记,而其他寄存器操作时,会先去lock寄存器读取其使用状态来确定是否能进行相关操作,从而确保防止冲突及达成一致性。
) I( }! Y# G! U0 |————————————————
0 M# Y; f2 y9 [5 I! f版权声明:py_free-物联智能( e: l [ ]0 C, W$ _ I4 R6 n! |
如有侵权请联系删除
/ z9 s+ g l9 ?% w: a2 k* n |